diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 657e65a22..000000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = tab -indent_size = 4 -end_of_line = lf -insert_final_newline = true - -[*.yml] -indent_style = space -indent_size = 2 - -[*.java] -ij_java_names_count_to_use_import_on_demand = 999 -ij_java_class_count_to_use_import_on_demand = 999 diff --git a/.github/ISSUE_TEMPLATE/new-issue.md b/.github/ISSUE_TEMPLATE/new-issue.md deleted file mode 100644 index a67364d11..000000000 --- a/.github/ISSUE_TEMPLATE/new-issue.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: New Issue -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -Thanks for considering raising a Spring Cloud Stream issue. Please take the time to review the following categories as some of them do not apply here. - -πŸ™… "Please DO NOT Raise an Issue" Cases -- Question -STOP!! Please ask questions about how to use something, or to understand why something isn't working as you expect it to, on Stack Overflow using the spring-cloud-stream tag. -- Security Vulnerability -STOP!! Please don't raise security vulnerabilities here. Head over to https://pivotal.io/security to learn how to disclose them responsibly. -- With an Immediate Pull Request -An issue will be closed as a duplicate of the immediate pull request, so you don't have to raise an issue if you plan to create a pull request immediately. - -If your case does not match any of he above categories. . . - -**Describe the issue** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Version of the framework** -**Expected behavior** -**Screenshots** - -**Additional context** -Add any other context about the problem here. diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..6921fd132 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,53 @@ +name: Deploy Docs +run-name: ${{ format('{0} ({1})', github.workflow, github.event.inputs.build-refname || 'all') }} +on: + workflow_dispatch: + inputs: + build-refname: + description: Enter git refname to build (e.g., 5.7.x). + required: false + push: + branches: docs-build +env: + GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} +permissions: + contents: write +jobs: + build: + if: github.repository_owner == 'spring-cloud' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 5 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Set up refname build + if: github.event.inputs.build-refname + run: | + git fetch --depth 1 https://github.com/$GITHUB_REPOSITORY ${{ github.event.inputs.build-refname }} + export BUILD_REFNAME=${{ github.event.inputs.build-refname }} + echo "BUILD_REFNAME=$BUILD_REFNAME" >> $GITHUB_ENV + export BUILD_VERSION=$(git cat-file --textconv FETCH_HEAD:pom.xml | python3 -c "import xml.etree.ElementTree as xml; from sys import stdin; print(xml.parse(stdin).getroot().find('{http://maven.apache.org/POM/4.0.0}version').text)") + echo BUILD_VERSION=$BUILD_VERSION >> $GITHUB_ENV + - name: Run Antora + run: | + ./mvnw --no-transfer-progress -B antora + - name: Publish Docs + uses: rwinch/spring-doc-actions/rsync-antora-reference@httpdocs-path + with: + docs-username: ${{ secrets.DOCS_USERNAME }} + docs-host: ${{ secrets.DOCS_HOST }} + docs-ssh-key: ${{ secrets.DOCS_SSH_KEY }} + docs-ssh-host-key: ${{ secrets.DOCS_SSH_HOST_KEY }} + site-path: target/antora/site + - name: Bust Cloudflare Cache + uses: spring-io/spring-doc-actions/bust-cloudflare-antora-cache@v0.0.11 + with: + context-root: spring-cloud-stream + cloudflare-zone-id: ${{ secrets.CLOUDFLARE_ZONE_ID }} + cloudflare-cache-token: ${{ secrets.CLOUDFLARE_CACHE_TOKEN }} diff --git a/.gitignore b/.gitignore index 214f0bcb3..6be8a4a16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,22 @@ -/application.yml -/application.properties -asciidoctor.css -*~ -.#* -*# target/ -build/ -bin/ -_site/ -.classpath +.settings/ .project -.settings +.classpath +*.orig .springBeans -.sts4-cache/ -.attach_pid* +.factorypath +.sts4-cache +.ant-targets-build.xml +src/ant/.ant-targets-upload-dist.xml +*.sonar4clipse* .DS_Store -*.sw* *.iml *.ipr *.iws -.idea/* -.factorypath -dump.rdb -.apt_generated -artifacts -**/dependency-reduced-pom.xml +/.idea/ +*.graphml +node +node_modules +build +package.json +package-lock.json diff --git a/.jdk8 b/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/.mvn/maven.config b/.mvn/maven.config deleted file mode 100644 index 3b8cf46e1..000000000 --- a/.mvn/maven.config +++ /dev/null @@ -1 +0,0 @@ --DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring diff --git a/.settings.xml b/.settings.xml deleted file mode 100644 index 03645e8ce..000000000 --- a/.settings.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - repo.spring.io - ${env.CI_DEPLOY_USERNAME} - ${env.CI_DEPLOY_PASSWORD} - - - - - - spring - - true - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - - - diff --git a/.springformat b/.springformat deleted file mode 100644 index e69de29bb..000000000 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e63f59f85..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -sudo: required -cache: - directories: - - $HOME/.m2 -language: java -jdk: - - oraclejdk8 -install: true -# The environment variable ${TRAVIS_PULL_REQUEST} is set to "false" when the build -# is for a normal branch commit. When the build is for a pull request, it will -# contain the pull request’s number. -script: - - '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || ./mvnw package -Pspring,full -U -Dmaven.test.redirectTestOutputToFile=false' - - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] || ./mvnw package -Pspring -U -Dmaven.test.redirectTestOutputToFile=false' -after_success: - - bash <(curl -s https://codecov.io/bash) -dist: trusty diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc deleted file mode 100644 index f013d6f36..000000000 --- a/CODE_OF_CONDUCT.adoc +++ /dev/null @@ -1,44 +0,0 @@ -= Contributor Code of Conduct - -As contributors and maintainers of this project, and in the interest of fostering an open -and welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or -patches, and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical or electronic addresses, - without explicit permission -* Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this -Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors -that they deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This Code of Conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will -be reviewed and investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. Maintainers are obligated to maintain confidentiality -with regard to the reporter of an incident. - -This Code of Conduct is adapted from the -http://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at -http://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8f71f43fe..000000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/README.adoc b/README.adoc index 0ab25fa7c..4ff7a2d99 100644 --- a/README.adoc +++ b/README.adoc @@ -1,409 +1,23 @@ -//// -DO NOT EDIT THIS FILE. IT WAS GENERATED. -Manual changes to this file will be lost when it is generated again. -Edit the files in the src/main/asciidoc/ directory instead. -//// += Spring Cloud Stream Docs Build +You're currently viewing the Antora playbook branch. +The playbook branch hosts the docs build that is used to build and publish the production docs site. -:jdkversion: 17 -:github-tag: master -:github-repo: spring-cloud/spring-cloud-stream +The Spring Cloud Stream reference docs are built using https://antora.org[Antora]. +This README covers how to build the docs in a software branch as well as how to build the production docs site locally. -:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} -:github-code: https://github.com/{github-repo}/tree/{github-tag} +== Building the Site -image::https://circleci.com/gh/spring-cloud/spring-cloud-stream.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-stream"] -image::https://codecov.io/gh/spring-cloud/spring-cloud-stream/branch/{github-tag}/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-stream"] -image::https://badges.gitter.im/spring-cloud/spring-cloud-stream.svg[Gitter, link="https://gitter.im/spring-cloud/spring-cloud-stream?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] +You can build the entire site by invoking the following on the docs-build branch and then viewing the site at `target/site/index.html` -// ====================================================================================== - -== Introduction -Spring Cloud Stream is a framework for building message-driven microservice applications. -Spring Cloud Stream builds upon Spring Boot to create standalone, production-grade Spring applications and uses Spring Integration to provide connectivity to message brokers. -It provides opinionated configuration of middleware from several vendors, introducing the concepts of persistent publish-subscribe semantics, consumer groups, and partitions. -These are called binder implementations in the parlance of Spring Cloud Stream. -Out of the box, Spring Cloud Stream provides binder implementations for Apache Kafka and RabbitMQ. -While these two binder implementations are based on Message Channels, Spring Cloud Stream also provides another binder implementation for Kafka Streams that does not use message channels, but native Kafka Streams types such as KStream, KTable etc. - -Below, you can find more information on how to use these various out-of-the-box binder implementations in your applications. - -== Apache Kafka Binder - -=== Usage - -To use Apache Kafka binder, you need to add `spring-cloud-stream-binder-kafka` as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven: - -[source,xml] +[source,bash] ---- - - org.springframework.cloud - spring-cloud-stream-binder-kafka - +./mvnw antora ---- -Alternatively, you can also use the Spring Cloud Stream Kafka Starter, as shown in the following example for Maven: +== Building a Specific Branch -[source,xml] +[source,bash] ---- - - org.springframework.cloud - spring-cloud-starter-stream-kafka - +./mvnw antora ---- - -== Apache Kafka Streams Binder - -=== Usage - -To use Apache Kafka Streams binder, you need to add `spring-cloud-stream-binder-kafka-streams` as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-kafka-streams - ----- - -== RabbitMQ Binder -=== Usage - -To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-rabbit - ----- - -Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-stream-rabbit - ----- - -== Resources - -For more information, please visit the https://spring.io/projects/spring-cloud-stream[project website]: - -== Building - -:jdkversion: 17 - -=== Basic Compile and Test - -To build the source you will need to install JDK {jdkversion}. - -Spring Cloud uses Maven for most build-related activities, and you -should be able to get off the ground quite quickly by cloning the -project you are interested in and typing - ----- -$ ./mvnw install ----- - -NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command -in place of `./mvnw` in the examples below. If you do that you also -might need to add `-P spring` if your local Maven settings do not -contain repository declarations for spring pre-release artifacts. - -NOTE: Be aware that you might need to increase the amount of memory -available to Maven by setting a `MAVEN_OPTS` environment variable with -a value like `-Xmx512m -XX:MaxPermSize=128m`. We try to cover this in -the `.mvn` configuration, so if you find you have to do it to make a -build succeed, please raise a ticket to get the settings added to -source control. - -The projects that require middleware (i.e. Redis) for testing generally -require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running. - - -=== Documentation - -The spring-cloud-build module has a "docs" profile, and if you switch -that on it will try to build asciidoc sources from -`src/main/asciidoc`. As part of that process it will look for a -`README.adoc` and process it by loading all the includes, but not -parsing or rendering it, just copying it to `${main.basedir}` -(defaults to `${basedir}`, i.e. the root of the project). If there are -any changes in the README it will then show up after a Maven build as -a modified file in the correct place. Just commit it and push the change. - -=== Working with the code -If you don't have an IDE preference we would recommend that you use -https://www.springsource.com/developer/sts[Spring Tools Suite] or -https://eclipse.org[Eclipse] when working with the code. We use the -https://eclipse.org/m2e/[m2eclipse] eclipse plugin for maven support. Other IDEs and tools -should also work without issue as long as they use Maven 3.3.3 or better. - -==== Activate the Spring Maven profile -Spring Cloud projects require the 'spring' Maven profile to be activated to resolve -the spring milestone and snapshot repositories. Use your preferred IDE to set this -profile to be active, or you may experience build errors. - -==== Importing into eclipse with m2eclipse -We recommend the https://eclipse.org/m2e/[m2eclipse] eclipse plugin when working with -eclipse. If you don't already have m2eclipse installed it is available from the "eclipse -marketplace". - -NOTE: Older versions of m2e do not support Maven 3.3, so once the -projects are imported into Eclipse you will also need to tell -m2eclipse to use the right profile for the projects. If you -see many different errors related to the POMs in the projects, check -that you have an up to date installation. If you can't upgrade m2e, -add the "spring" profile to your `settings.xml`. Alternatively you can -copy the repository settings from the "spring" profile of the parent -pom into your `settings.xml`. - -==== Importing into eclipse without m2eclipse -If you prefer not to use m2eclipse you can generate eclipse project metadata using the -following command: - -[indent=0] ----- - $ ./mvnw eclipse:eclipse ----- - -The generated eclipse projects can be imported by selecting `import existing projects` -from the `file` menu. - - -== Contributing - -:spring-cloud-build-branch: master - -Spring Cloud is released under the non-restrictive Apache 2.0 license, -and follows a very standard Github development process, using Github -tracker for issues and merging pull requests into master. If you want -to contribute even something trivial please do not hesitate, but -follow the guidelines below. - -=== Sign the Contributor License Agreement -Before we accept a non-trivial patch or pull request we will need you to sign the -https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -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 of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of -conduct]. By participating, you are expected to uphold this code. Please report -unacceptable behavior to spring-code-of-conduct@pivotal.io. - -=== 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. - -* Use the Spring Framework code format conventions. If you use Eclipse - you can import formatter settings using the - `eclipse-code-formatter.xml` file from the - https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring - Cloud Build] project. If using IntelliJ, you can use the - https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter - Plugin] to import the same file. -* Make sure all new `.java` files to have a simple Javadoc class comment with at least an - `@author` tag identifying you, and preferably at least a paragraph on what the class is - for. -* Add the ASF license header comment to all new `.java` files (copy from existing files - in the project) -* Add yourself as an `@author` to the .java files that you modify substantially (more - than cosmetic changes). -* Add some Javadocs and, if you change the namespace, some XSD doc elements. -* A few unit tests would help a lot as well -- someone has to do it. -* If no-one else is using your branch, please rebase it against the current master (or - other target branch in the main project). -* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], - if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit - message (where XXXX is the issue number). - -=== Checkstyle - -Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are: - -.spring-cloud-build-tools/ ----- -└── src - Β Β  β”œβ”€β”€ checkstyle - Β Β  β”‚Β Β  └── checkstyle-suppressions.xml <3> - Β Β  └── main - Β Β  └── resources - Β Β  β”œβ”€β”€ checkstyle-header.txt <2> - Β Β  └── checkstyle.xml <1> ----- -<1> Default Checkstyle rules -<2> File header setup -<3> Default suppression rules - -==== Checkstyle configuration - -Checkstyle rules are *disabled by default*. To add checkstyle to your project just define the following properties and plugins. - -.pom.xml ----- - -true <1> - true - <2> - true - <3> - - - - - <4> - io.spring.javaformat - spring-javaformat-maven-plugin - - <5> - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - <5> - org.apache.maven.plugins - maven-checkstyle-plugin - - - - ----- -<1> Fails the build upon Checkstyle errors -<2> Fails the build upon Checkstyle violations -<3> Checkstyle analyzes also the test sources -<4> Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules -<5> Add checkstyle plugin to your build and reporting phases - -If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example: - -.projectRoot/src/checkstyle/checkstyle-suppresions.xml ----- - - - - - - ----- - -It's advisable to copy the `${spring-cloud-build.rootFolder}/.editorconfig` and `${spring-cloud-build.rootFolder}/.springformat` to your project. That way, some default formatting rules will be applied. You can do so by running this script: - -```bash -$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig -$ touch .springformat -``` - -=== IDE setup - -==== Intellij IDEA - -In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. -The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools[Spring Cloud Build] project. - -.spring-cloud-build-tools/ ----- -└── src - Β Β  β”œβ”€β”€ checkstyle - Β Β  β”‚Β Β  └── checkstyle-suppressions.xml <3> - Β Β  └── main - Β Β  └── resources - Β Β  β”œβ”€β”€ checkstyle-header.txt <2> - Β Β  β”œβ”€β”€ checkstyle.xml <1> - Β Β  └── intellij - Β Β  Β Β  β”œβ”€β”€ Intellij_Project_Defaults.xml <4> - Β Β  Β Β  └── Intellij_Spring_Boot_Java_Conventions.xml <5> ----- -<1> Default Checkstyle rules -<2> File header setup -<3> Default suppression rules -<4> Project defaults for Intellij that apply most of Checkstyle rules -<5> Project style conventions for Intellij that apply most of Checkstyle rules - -.Code style - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-code-style.png[Code style] - -Go to `File` -> `Settings` -> `Editor` -> `Code style`. There click on the icon next to the `Scheme` section. There, click on the `Import Scheme` value and pick the `Intellij IDEA code style XML` option. Import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml` file. - -.Inspection profiles - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-inspections.png[Code style] - -Go to `File` -> `Settings` -> `Editor` -> `Inspections`. There click on the icon next to the `Profile` section. There, click on the `Import Profile` and import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml` file. - -.Checkstyle - -To have Intellij work with Checkstyle, you have to install the `Checkstyle` plugin. It's advisable to also install the `Assertions2Assertj` to automatically convert the JUnit assertions - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-checkstyle.png[Checkstyle] - -Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables: - -- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL. -- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL. -- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`. - -IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources. - -=== Duplicate Finder - -Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath. - -==== Duplicate Finder configuration - -Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`. - -.pom.xml -[source,xml] ----- - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - ----- - -For other properties, we have set defaults as listed in the https://github.com/basepom/duplicate-finder-maven-plugin/wiki[plugin documentation]. - -You can easily override them but setting the value of the selected property prefixed with `duplicate-finder-maven-plugin`. For example, set `duplicate-finder-maven-plugin.skip` to `true` in order to skip duplicates check in your build. - -If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your setup, make sure to add them in the plugin configuration section of your project: - -[source,xml] ----- - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - org.joda.time.base.BaseDateTime - .*module-info - - - changelog.txt - - - - - - - ----- - diff --git a/docs/antora-playbook.yml b/antora-playbook.yml similarity index 61% rename from docs/antora-playbook.yml rename to antora-playbook.yml index 9a70e676c..49c71eedc 100644 --- a/docs/antora-playbook.yml +++ b/antora-playbook.yml @@ -6,38 +6,39 @@ antora: - '@antora/collector-extension' - '@antora/atlas-extension' - require: '@springio/antora-extensions/root-component-extension' - root_component_name: 'PROJECT_WITHOUT_SPRING' - # FIXME: Run antora once using this extension to migrate to the Asciidoc Tabs syntax - # and then remove this extension - - require: '@springio/antora-extensions/tabs-migration-extension' - unwrap_example_block: always - save_result: true + root_component_name: 'cloud-stream' site: - title: PROJECT_FULL_NAME - url: https://docs.spring.io/PROJECT_NAME/reference/ + title: Spring Cloud Stream + url: https://docs.spring.io/spring-cloud-stream/reference + robots: allow +git: + ensure_git_suffix: false content: sources: - - url: ./.. - branches: HEAD + - url: https://github.com/spring-cloud/spring-cloud-stream + # Refname matching: + # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ + branches: [ main ] + tags: [ '({4..9}).+({1..9}).+({0..9})?(-{RC,M}+({0..9}))', '!4.1.0-M1' ] start_path: docs - worktrees: true asciidoc: attributes: page-stackoverflow-url: https://stackoverflow.com/tags/spring-cloud page-pagination: '' hide-uri-scheme: '@' tabs-sync-option: '@' - chomp: 'all' extensions: - '@asciidoctor/tabs' - '@springio/asciidoctor-extensions' - sourcemap: true urls: + latest_version_segment_strategy: redirect:to latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip + snapshot: true runtime: log: failure_level: warn format: pretty -ui: - bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.3.5/ui-bundle.zip diff --git a/binders/kafka-binder/.gitignore b/binders/kafka-binder/.gitignore deleted file mode 100644 index 8c6847291..000000000 --- a/binders/kafka-binder/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -/application.yml -/application.properties -asciidoctor.css -*~ -.#* -*# -target/ -build/ -bin/ -_site/ -.classpath -.project -.settings -.springBeans -.DS_Store -*.sw* -*.iml -*.ipr -*.iws -.idea/* -*/.idea -.factorypath -dump.rdb -.apt_generated -artifacts -.sts4-cache diff --git a/binders/kafka-binder/.jdk8 b/binders/kafka-binder/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/.mvn/jvm.config b/binders/kafka-binder/.mvn/jvm.config deleted file mode 100644 index 0e7dabeff..000000000 --- a/binders/kafka-binder/.mvn/jvm.config +++ /dev/null @@ -1 +0,0 @@ --Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom \ No newline at end of file diff --git a/binders/kafka-binder/.mvn/maven.config b/binders/kafka-binder/.mvn/maven.config deleted file mode 100644 index 3b8cf46e1..000000000 --- a/binders/kafka-binder/.mvn/maven.config +++ /dev/null @@ -1 +0,0 @@ --DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring diff --git a/binders/kafka-binder/.mvn/wrapper/MavenWrapperDownloader.java b/binders/kafka-binder/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index b901097f2..000000000 --- a/binders/kafka-binder/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/binders/kafka-binder/.mvn/wrapper/maven-wrapper.jar b/binders/kafka-binder/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a55..000000000 Binary files a/binders/kafka-binder/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/binders/kafka-binder/.mvn/wrapper/maven-wrapper.properties b/binders/kafka-binder/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572ce..000000000 --- a/binders/kafka-binder/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/binders/kafka-binder/.settings.xml b/binders/kafka-binder/.settings.xml deleted file mode 100644 index 3da1ea205..000000000 --- a/binders/kafka-binder/.settings.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - repo.spring.io - ${env.CI_DEPLOY_USERNAME} - ${env.CI_DEPLOY_PASSWORD} - - - - - - spring - true - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - - - diff --git a/binders/kafka-binder/LICENSE b/binders/kafka-binder/LICENSE deleted file mode 100644 index 9b259bdfc..000000000 --- a/binders/kafka-binder/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/binders/kafka-binder/mvnw b/binders/kafka-binder/mvnw deleted file mode 100755 index 41c0f0c23..000000000 --- a/binders/kafka-binder/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - 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" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/binders/kafka-binder/mvnw.cmd b/binders/kafka-binder/mvnw.cmd deleted file mode 100644 index 86115719e..000000000 --- a/binders/kafka-binder/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/binders/kafka-binder/pom.xml b/binders/kafka-binder/pom.xml deleted file mode 100644 index 796019ee8..000000000 --- a/binders/kafka-binder/pom.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - 4.0.0 - spring-cloud-stream-binder-kafka-parent - 4.1.0-SNAPSHOT - pom - - org.springframework.cloud - spring-cloud-stream-binders - 4.1.0-SNAPSHOT - - - https://github.com/spring-cloud/spring-cloud-stream-binder-kafka - scm:git:git://github.com/spring-cloud/spring-cloud-stream-binder-kafka.git - - - scm:git:ssh://git@github.com/spring-cloud/spring-cloud-stream-binder-kafka.git - - HEAD - - - 17 - true - true - true - - - spring-cloud-stream-binder-kafka - spring-cloud-starter-stream-kafka - spring-cloud-stream-binder-kafka-core - spring-cloud-stream-binder-kafka-streams - spring-cloud-stream-binder-kafka-reactive - - - - - org.junit.vintage - junit-vintage-engine - test - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M7 - - true - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - -parameters - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - - spring - - - - - coverage - - - env.TRAVIS - true - - - - - - org.jacoco - jacoco-maven-plugin - 0.7.9 - - - agent - - prepare-agent - - - - report - test - - report - - - - - - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - - spring-milestones - Spring milestones - https://repo.spring.io/libs-milestone-local - - - rsocket-snapshots - RSocket Snapshots - https://oss.jfrog.org/oss-snapshot-local - - true - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - diff --git a/binders/kafka-binder/spring-cloud-starter-stream-kafka/.jdk8 b/binders/kafka-binder/spring-cloud-starter-stream-kafka/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/spring-cloud-starter-stream-kafka/pom.xml b/binders/kafka-binder/spring-cloud-starter-stream-kafka/pom.xml deleted file mode 100644 index 0195eccfa..000000000 --- a/binders/kafka-binder/spring-cloud-starter-stream-kafka/pom.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - 4.0.0 - - org.springframework.cloud - spring-cloud-stream-binder-kafka-parent - 4.1.0-SNAPSHOT - - spring-cloud-starter-stream-kafka - Spring Cloud Starter Stream Kafka - https://projects.spring.io/spring-cloud - - Pivotal Software, Inc. - https://www.spring.io - - - ${basedir}/../.. - - - - org.springframework.cloud - spring-cloud-stream-binder-kafka - ${project.version} - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/.jdk8 b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/pom.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/pom.xml deleted file mode 100644 index 6091fcbd4..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - 4.0.0 - - org.springframework.cloud - spring-cloud-stream-binder-kafka-parent - 4.1.0-SNAPSHOT - - spring-cloud-stream-binder-kafka-core - Spring Cloud Stream Kafka Binder Core - https://projects.spring.io/spring-cloud - - Pivotal Software, Inc. - https://www.spring.io - - - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.integration - spring-integration-kafka - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.kafka - spring-kafka-test - test - - - org.springframework.boot - spring-boot-starter-test - test - - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/AbstractKafkaBinderHealthIndicator.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/AbstractKafkaBinderHealthIndicator.java deleted file mode 100644 index e4cf87d8b..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/AbstractKafkaBinderHealthIndicator.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.common; - -import java.time.Duration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.common.PartitionInfo; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.actuate.health.StatusAggregator; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.util.Assert; - -/** - * Base class that abstracts the common health indicator details for the various Kafka binder flavors. - * - * @author Soby Chacko - * @since 4.1.0 - */ -public abstract class AbstractKafkaBinderHealthIndicator extends AbstractHealthIndicator implements DisposableBean { - - private static final int DEFAULT_TIMEOUT = 60; - - protected int timeout = DEFAULT_TIMEOUT; - - private final ExecutorService executor; - - protected Consumer metadataConsumer; - - protected boolean considerDownWhenAnyPartitionHasNoLeader; - - private final ConsumerFactory consumerFactory; - - public AbstractKafkaBinderHealthIndicator(ConsumerFactory consumerFactory) { - this.consumerFactory = consumerFactory; - this.executor = createHealthBinderExecutorService(); - Assert.notNull(this.executor, "The health indicator executor service must not be null"); - } - - protected abstract Map getTopicsInUse(); - - protected abstract Health buildBinderSpecificHealthDetails(); - - protected abstract ExecutorService createHealthBinderExecutorService(); - - private void initMetadataConsumer() { - if (this.metadataConsumer == null) { - this.metadataConsumer = this.consumerFactory.createConsumer(); - } - } - - @Override - public void destroy() { - executor.shutdown(); - if (this.metadataConsumer != null) { - this.metadataConsumer.close(); - } - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - Health topicsHealth = safelyBuildTopicsHealth(); - Health listenerContainersHealth = buildBinderSpecificHealthDetails(); - merge(topicsHealth, listenerContainersHealth, builder); - } - - protected Health safelyBuildTopicsHealth() { - Future future = executor.submit(this::buildTopicsHealth); - try { - return future.get(this.timeout, TimeUnit.SECONDS); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - return Health.down() - .withDetail("Interrupted while waiting for partition information in", - this.timeout + " seconds") - .build(); - } - catch (ExecutionException ex) { - return Health.down(ex).build(); - } - catch (TimeoutException ex) { - return Health.down().withDetail("Failed to retrieve partition information in", - this.timeout + " seconds").build(); - } - } - - private Health buildTopicsHealth() { - try { - initMetadataConsumer(); - Set downMessages = new HashSet<>(); - Set checkedTopics = new HashSet<>(); - final Map topicsInUse = getTopicsInUse(); - if (topicsInUse.isEmpty()) { - try { - this.metadataConsumer.listTopics(Duration.ofSeconds(this.timeout)); - } - catch (Exception e) { - return Health.down().withDetail("No topic information available", - "Kafka broker is not reachable").build(); - } - return Health.unknown().withDetail("No bindings found", - "Kafka binder may not be bound to destinations on the broker").build(); - } - else { - for (String topic : topicsInUse.keySet()) { - TopicInformation topicInformation = topicsInUse - .get(topic); - if (!topicInformation.isTopicPattern()) { - List partitionInfos = this.metadataConsumer - .partitionsFor(topic); - for (PartitionInfo partitionInfo : partitionInfos) { - if (topicInformation.partitionInfos() - .contains(partitionInfo) - && partitionInfo.leader() == null || - (partitionInfo.leader() != null && partitionInfo.leader().id() == -1)) { - downMessages.add(partitionInfo.toString()); - } - else if (this.considerDownWhenAnyPartitionHasNoLeader && - partitionInfo.leader() == null || (partitionInfo.leader() != null && partitionInfo.leader().id() == -1)) { - downMessages.add(partitionInfo.toString()); - } - } - checkedTopics.add(topic); - } - else { - try { - // Since destination is a pattern, all we are doing is just to make sure that - // we can connect to the cluster and query the topics. - this.metadataConsumer.listTopics(Duration.ofSeconds(this.timeout)); - } - catch (Exception ex) { - return Health.down() - .withDetail("Cluster not connected", - "Destination provided is a pattern, but cannot connect to the cluster for any verification") - .build(); - } - } - } - } - if (downMessages.isEmpty()) { - return Health.up().withDetail("topicsInUse", checkedTopics).build(); - } - else { - return Health.down() - .withDetail("Following partitions in use have no leaders: ", - downMessages.toString()) - .build(); - } - } - catch (Exception ex) { - return Health.down(ex).build(); - } - } - - private void merge(Health topicsHealth, Health listenerContainersHealth, Health.Builder builder) { - Status aggregatedStatus = StatusAggregator.getDefault() - .getAggregateStatus(topicsHealth.getStatus(), listenerContainersHealth.getStatus()); - Map aggregatedDetails = new HashMap<>(); - aggregatedDetails.putAll(topicsHealth.getDetails()); - aggregatedDetails.putAll(listenerContainersHealth.getDetails()); - builder.status(aggregatedStatus).withDetails(aggregatedDetails); - } - - /** - * Set the timeout in seconds to retrieve health information. - * - * @param timeout the timeout - default 60. - */ - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - public void setConsiderDownWhenAnyPartitionHasNoLeader(boolean considerDownWhenAnyPartitionHasNoLeader) { - this.considerDownWhenAnyPartitionHasNoLeader = considerDownWhenAnyPartitionHasNoLeader; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/KafkaBinderEnvironmentPostProcessor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/KafkaBinderEnvironmentPostProcessor.java deleted file mode 100644 index 203afb9b0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/KafkaBinderEnvironmentPostProcessor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.common; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.apache.kafka.common.serialization.ByteArraySerializer; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; - -/** - * An {@link EnvironmentPostProcessor} that sets some common configuration properties (log - * config etc.,) for Kafka binder. - * - * @author Ilayaperumal Gopinathan - */ -public class KafkaBinderEnvironmentPostProcessor implements EnvironmentPostProcessor { - - private static final String SPRING_KAFKA = "spring.kafka"; - - private static final String SPRING_KAFKA_PRODUCER = SPRING_KAFKA + ".producer"; - - private static final String SPRING_KAFKA_CONSUMER = SPRING_KAFKA + ".consumer"; - - private static final String SPRING_KAFKA_PRODUCER_KEY_SERIALIZER = SPRING_KAFKA_PRODUCER - + "." + "keySerializer"; - - private static final String SPRING_KAFKA_PRODUCER_VALUE_SERIALIZER = SPRING_KAFKA_PRODUCER - + "." + "valueSerializer"; - - private static final String SPRING_KAFKA_CONSUMER_KEY_DESERIALIZER = SPRING_KAFKA_CONSUMER - + "." + "keyDeserializer"; - - private static final String SPRING_KAFKA_CONSUMER_VALUE_DESERIALIZER = SPRING_KAFKA_CONSUMER - + "." + "valueDeserializer"; - - private static final String KAFKA_BINDER_DEFAULT_PROPERTIES = "kafkaBinderDefaultProperties"; - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, - SpringApplication application) { - if (!environment.getPropertySources().contains(KAFKA_BINDER_DEFAULT_PROPERTIES)) { - Map kafkaBinderDefaultProperties = new HashMap<>(); - kafkaBinderDefaultProperties.put("logging.level.org.I0Itec.zkclient", - "ERROR"); - kafkaBinderDefaultProperties.put("logging.level.kafka.server.KafkaConfig", - "ERROR"); - kafkaBinderDefaultProperties - .put("logging.level.kafka.admin.AdminClient.AdminConfig", "ERROR"); - kafkaBinderDefaultProperties.put(SPRING_KAFKA_PRODUCER_KEY_SERIALIZER, - ByteArraySerializer.class.getName()); - kafkaBinderDefaultProperties.put(SPRING_KAFKA_PRODUCER_VALUE_SERIALIZER, - ByteArraySerializer.class.getName()); - kafkaBinderDefaultProperties.put(SPRING_KAFKA_CONSUMER_KEY_DESERIALIZER, - ByteArrayDeserializer.class.getName()); - kafkaBinderDefaultProperties.put(SPRING_KAFKA_CONSUMER_VALUE_DESERIALIZER, - ByteArrayDeserializer.class.getName()); - environment.getPropertySources().addLast(new MapPropertySource( - KAFKA_BINDER_DEFAULT_PROPERTIES, kafkaBinderDefaultProperties)); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/TopicInformation.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/TopicInformation.java deleted file mode 100644 index bac2b6901..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/common/TopicInformation.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.common; - -import java.util.Collection; - -import org.apache.kafka.common.PartitionInfo; - -/** - * Record to capture topic information for various binder related tasks. - * - * @param consumerGroup consumer group for the consumer - * @param partitionInfos collection of {@link PartitionInfo} - * @param isTopicPattern if the topic is specified as a pattern - * - * @author Soby Chacko (and previous authors before refactoring). - */ -public record TopicInformation(String consumerGroup, Collection partitionInfos, boolean isTopicPattern) { - - public boolean isConsumerTopic() { - return this.consumerGroup != null; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/JaasLoginModuleConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/JaasLoginModuleConfiguration.java deleted file mode 100644 index 9934f30dd..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/JaasLoginModuleConfiguration.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.properties; - -import java.util.HashMap; -import java.util.Map; - -import javax.security.auth.login.AppConfigurationEntry; - -import org.springframework.kafka.security.jaas.KafkaJaasLoginModuleInitializer; -import org.springframework.util.Assert; - -/** - * Contains properties for setting up an {@link AppConfigurationEntry} that can be used - * for the Kafka or Zookeeper client. - * - * @author Marius Bogoevici - * @author Soby Chacko - */ -public class JaasLoginModuleConfiguration { - - private String loginModule = "com.sun.security.auth.module.Krb5LoginModule"; - - private KafkaJaasLoginModuleInitializer.ControlFlag controlFlag = KafkaJaasLoginModuleInitializer.ControlFlag.REQUIRED; - - private Map options = new HashMap<>(); - - public String getLoginModule() { - return this.loginModule; - } - - public void setLoginModule(String loginModule) { - Assert.notNull(loginModule, "cannot be null"); - this.loginModule = loginModule; - } - - public KafkaJaasLoginModuleInitializer.ControlFlag getControlFlag() { - return this.controlFlag; - } - - public void setControlFlag(String controlFlag) { - Assert.notNull(controlFlag, "cannot be null"); - this.controlFlag = KafkaJaasLoginModuleInitializer.ControlFlag - .valueOf(controlFlag.toUpperCase()); - } - - public Map getOptions() { - return this.options; - } - - public void setOptions(Map options) { - this.options = options; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBinderConfigurationProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBinderConfigurationProperties.java deleted file mode 100644 index 13beac13f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBinderConfigurationProperties.java +++ /dev/null @@ -1,723 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.properties; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import jakarta.validation.constraints.AssertTrue; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.producer.ProducerConfig; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.HeaderMode; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties.CompressionType; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.expression.Expression; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * Configuration properties for the Kafka binder. The properties in this class are - * prefixed with spring.cloud.stream.kafka.binder. - * - * @author David Turanski - * @author Ilayaperumal Gopinathan - * @author Marius Bogoevici - * @author Soby Chacko - * @author Gary Russell - * @author Rafal Zukowski - * @author Aldo Sinanaj - * @author Lukasz Kaminski - * @author Chukwubuikem Ume-Ugwa - * @author Nico Heller - * @author Norbert Gyurian - */ -public class KafkaBinderConfigurationProperties { - - private static final String DEFAULT_KAFKA_CONNECTION_STRING = "localhost:9092"; - - private final Log logger = LogFactory.getLog(getClass()); - - private final Transaction transaction = new Transaction(); - - private final Metrics metrics = new Metrics(); - - private final KafkaProperties kafkaProperties; - - /** - * Arbitrary kafka properties that apply to both producers and consumers. - */ - private Map configuration = new HashMap<>(); - - /** - * Arbitrary kafka consumer properties. - */ - private Map consumerProperties = new HashMap<>(); - - /** - * Arbitrary kafka producer properties. - */ - private Map producerProperties = new HashMap<>(); - - private String[] brokers = new String[] { "localhost" }; - - private String defaultBrokerPort = "9092"; - - private String[] headers = new String[] {}; - - private boolean autoCreateTopics = true; - - private boolean autoAlterTopics; - - private boolean autoAddPartitions; - - private boolean considerDownWhenAnyPartitionHasNoLeader; - - private String requiredAcks = "1"; - - private short replicationFactor = -1; - - private int minPartitionCount = 1; - - /** - * Time to wait to get partition information in seconds; default 60. - */ - private int healthTimeout = 60; - - private JaasLoginModuleConfiguration jaas; - - /** - * The bean name of a custom header mapper to use instead of a - * {@link org.springframework.kafka.support.DefaultKafkaHeaderMapper}. - */ - private String headerMapperBeanName; - - /** - * Time between retries after AuthorizationException is caught in - * the ListenerContainer; defalt is null which disables retries. - * For more info see: {@link org.springframework.kafka.listener.ConsumerProperties#setAuthorizationExceptionRetryInterval(java.time.Duration)} - */ - private Duration authorizationExceptionRetryInterval; - - /** - * When a certificate store location is not given as a local file system path, then the binder - * converts the path to {@link org.springframework.core.io.Resource} resource, then copies - * the resource from the original location to a location on - * the filesystem. If this value is set, then this location is used, otherwise, the - * certificate file is copied to the directory returned by java.io.tmpdir. - */ - private String certificateStoreDirectory; - - /** - * Enable Micrometer observation registry across all the bindings in the binder. - */ - private boolean enableObservation; - - /** - * Earlier, @Autowired on this constructor was necessary for all the properties to be discovered - * and bound when running as a native application. However, now that Spring Boot fixed the underlying - * issue, we are removing the @Autowired from the constructor. See these Boot issues for more details. - * https://github.com/spring-projects/spring-boot/issues/34507 - * https://github.com/spring-projects/spring-boot/issues/35564 - * - * @param kafkaProperties Spring Kafka properties autoconfigured by Spring Boot - */ - public KafkaBinderConfigurationProperties(KafkaProperties kafkaProperties) { - Assert.notNull(kafkaProperties, "'kafkaProperties' cannot be null"); - this.kafkaProperties = kafkaProperties; - } - - public KafkaProperties getKafkaProperties() { - return this.kafkaProperties; - } - - public Transaction getTransaction() { - return this.transaction; - } - - public Metrics getMetrics() { - return metrics; - } - - public String getKafkaConnectionString() { - // We need to do a check on certificate file locations to see if they are given as non-local file system resources. - // If that is the case, then we will copy them to a file system location and use those as the certificate locations. - // This is due to a limitation in Kafka itself in which it doesn't allow reading certificate resources from non-local - // file system locations e.g. classpath, http. - // See this: https://issues.apache.org/jira/browse/KAFKA-7685 - // and this: https://cwiki.apache.org/confluence/display/KAFKA/KIP-398%3A+Support+reading+trust+store+from+classpath - moveCertsToFileSystemIfNecessary(); - - return toConnectionString(this.brokers, this.defaultBrokerPort); - } - - private void moveCertsToFileSystemIfNecessary() { - try { - // broker certificates - moveCertsIfApplicable("ssl.truststore.location"); - moveCertsIfApplicable("ssl.keystore.location"); - - // schema registry certificates - moveCertsIfApplicable("schema.registry.ssl.truststore.location"); - moveCertsIfApplicable("schema.registry.ssl.keystore.location"); - } - catch (Exception e) { - throw new IllegalStateException(e); - } - } - - private void moveCertsIfApplicable(String storeProperty) throws IOException { - final String storeLocation = this.configuration.get(storeProperty); - - // If the path is not defined, or it is a local file path do not move the file - if (storeLocation != null && !checkIfFileExists(storeLocation)) { - final String fileSystemLocation = moveCertToFileSystem(storeLocation, this.certificateStoreDirectory); - // Overriding the value with absolute filesystem path. - this.configuration.put(storeProperty, fileSystemLocation); - } - } - - private boolean checkIfFileExists(String path) { - try { - return Files.isRegularFile(Paths.get(path)); - } - catch (Exception e) { - return false; - } - } - - private String moveCertToFileSystem(String resourceLocation, String fileSystemLocation) throws IOException { - File targetFile; - final String tempDir = System.getProperty("java.io.tmpdir"); - Resource resource = new DefaultResourceLoader().getResource(resourceLocation); - if (StringUtils.hasText(fileSystemLocation)) { - final Path path = Paths.get(fileSystemLocation); - if (!Files.exists(path) || !Files.isDirectory(path) || !Files.isWritable(path)) { - logger.warn("The filesystem location to move the cert files (" + fileSystemLocation + ") " + - "is not found or a directory that is writable. The system temp folder (java.io.tmpdir) will be used instead."); - targetFile = new File(Paths.get(tempDir, resource.getFilename()).toString()); - } - else { - // the given location is verified to be a writable directory. - targetFile = new File(Paths.get(fileSystemLocation, resource.getFilename()).toString()); - } - } - else { - targetFile = new File(Paths.get(tempDir, resource.getFilename()).toString()); - } - - try (InputStream inputStream = resource.getInputStream()) { - Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - return targetFile.getAbsolutePath(); - } - - public String getDefaultKafkaConnectionString() { - return DEFAULT_KAFKA_CONNECTION_STRING; - } - - public String[] getHeaders() { - return this.headers; - } - - public String[] getBrokers() { - return this.brokers; - } - - public void setBrokers(String... brokers) { - this.brokers = brokers; - } - - public void setDefaultBrokerPort(String defaultBrokerPort) { - this.defaultBrokerPort = defaultBrokerPort; - } - - public void setHeaders(String... headers) { - this.headers = headers; - } - - /** - * Converts an array of host values to a comma-separated String. It will append the - * default port value, if not already specified. - * @param hosts host string - * @param defaultPort port - * @return formatted connection string - */ - private String toConnectionString(String[] hosts, String defaultPort) { - String[] fullyFormattedHosts = new String[hosts.length]; - for (int i = 0; i < hosts.length; i++) { - if (hosts[i].contains(":") || !StringUtils.hasText(defaultPort)) { - fullyFormattedHosts[i] = hosts[i]; - } - else { - fullyFormattedHosts[i] = hosts[i] + ":" + defaultPort; - } - } - return StringUtils.arrayToCommaDelimitedString(fullyFormattedHosts); - } - - public String getRequiredAcks() { - return this.requiredAcks; - } - - public void setRequiredAcks(String requiredAcks) { - this.requiredAcks = requiredAcks; - } - - public short getReplicationFactor() { - return this.replicationFactor; - } - - public void setReplicationFactor(short replicationFactor) { - this.replicationFactor = replicationFactor; - } - - public int getMinPartitionCount() { - return this.minPartitionCount; - } - - public void setMinPartitionCount(int minPartitionCount) { - this.minPartitionCount = minPartitionCount; - } - - public int getHealthTimeout() { - return this.healthTimeout; - } - - public void setHealthTimeout(int healthTimeout) { - this.healthTimeout = healthTimeout; - } - - public boolean isAutoCreateTopics() { - return this.autoCreateTopics; - } - - public void setAutoCreateTopics(boolean autoCreateTopics) { - this.autoCreateTopics = autoCreateTopics; - } - - public boolean isAutoAlterTopics() { - return autoAlterTopics; - } - - public void setAutoAlterTopics(boolean autoAlterTopics) { - this.autoAlterTopics = autoAlterTopics; - } - - public boolean isAutoAddPartitions() { - return this.autoAddPartitions; - } - - public void setAutoAddPartitions(boolean autoAddPartitions) { - this.autoAddPartitions = autoAddPartitions; - } - - public Map getConfiguration() { - return this.configuration; - } - - public void setConfiguration(Map configuration) { - this.configuration = configuration; - } - - public Map getConsumerProperties() { - return this.consumerProperties; - } - - public void setConsumerProperties(Map consumerProperties) { - Assert.notNull(consumerProperties, "'consumerProperties' cannot be null"); - this.consumerProperties = consumerProperties; - } - - public Map getProducerProperties() { - return this.producerProperties; - } - - public void setProducerProperties(Map producerProperties) { - Assert.notNull(producerProperties, "'producerProperties' cannot be null"); - this.producerProperties = producerProperties; - } - - /** - * Merge boot consumer properties, general properties from - * {@link #setConfiguration(Map)} that apply to consumers, properties from - * {@link #setConsumerProperties(Map)}, in that order. - * @return the merged properties. - */ - public Map mergedConsumerConfiguration() { - Map consumerConfiguration = new HashMap<>(); - consumerConfiguration.putAll(this.kafkaProperties.buildConsumerProperties()); - // Copy configured binder properties that apply to consumers - for (Map.Entry configurationEntry : this.configuration - .entrySet()) { - if (ConsumerConfig.configNames().contains(configurationEntry.getKey())) { - consumerConfiguration.put(configurationEntry.getKey(), - configurationEntry.getValue()); - } - } - consumerConfiguration.putAll(this.consumerProperties); - filterStreamManagedConfiguration(consumerConfiguration); - // Override Spring Boot bootstrap server setting if left to default with the value - // configured in the binder - return getConfigurationWithBootstrapServer(consumerConfiguration, - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG); - } - - /** - * Merge boot producer properties, general properties from - * {@link #setConfiguration(Map)} that apply to producers, properties from - * {@link #setProducerProperties(Map)}, in that order. - * @return the merged properties. - */ - public Map mergedProducerConfiguration() { - Map producerConfiguration = new HashMap<>(); - producerConfiguration.putAll(this.kafkaProperties.buildProducerProperties()); - // Copy configured binder properties that apply to producers - for (Map.Entry configurationEntry : this.configuration - .entrySet()) { - if (ProducerConfig.configNames().contains(configurationEntry.getKey())) { - producerConfiguration.put(configurationEntry.getKey(), - configurationEntry.getValue()); - } - } - producerConfiguration.putAll(this.producerProperties); - // Override Spring Boot bootstrap server setting if left to default with the value - // configured in the binder - return getConfigurationWithBootstrapServer(producerConfiguration, - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG); - } - - private void filterStreamManagedConfiguration(Map configuration) { - if (configuration.containsKey(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG) - && configuration.get(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG).toString().equalsIgnoreCase("true")) { - logger.warn(constructIgnoredConfigMessage(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG) + - ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG + "=true is not supported by the Kafka binder"); - configuration.remove(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG); - } - if (configuration.containsKey(ConsumerConfig.GROUP_ID_CONFIG)) { - logger.warn(constructIgnoredConfigMessage(ConsumerConfig.GROUP_ID_CONFIG) + - "Use spring.cloud.stream.default.group or spring.cloud.stream.binding..group to specify " + - "the group instead of " + ConsumerConfig.GROUP_ID_CONFIG); - configuration.remove(ConsumerConfig.GROUP_ID_CONFIG); - } - } - - private String constructIgnoredConfigMessage(String config) { - return String.format("Ignoring provided value(s) for '%s'. ", config); - } - - private Map getConfigurationWithBootstrapServer( - Map configuration, String bootstrapServersConfig) { - final String kafkaConnectionString = getKafkaConnectionString(); - if (ObjectUtils.isEmpty(configuration.get(bootstrapServersConfig)) || - !kafkaConnectionString.equals("localhost:9092")) { - configuration.put(bootstrapServersConfig, kafkaConnectionString); - } - return Collections.unmodifiableMap(configuration); - } - - public JaasLoginModuleConfiguration getJaas() { - return this.jaas; - } - - public void setJaas(JaasLoginModuleConfiguration jaas) { - this.jaas = jaas; - } - - public String getHeaderMapperBeanName() { - return this.headerMapperBeanName; - } - - public void setHeaderMapperBeanName(String headerMapperBeanName) { - this.headerMapperBeanName = headerMapperBeanName; - } - - public Duration getAuthorizationExceptionRetryInterval() { - return authorizationExceptionRetryInterval; - } - - public void setAuthorizationExceptionRetryInterval(Duration authorizationExceptionRetryInterval) { - this.authorizationExceptionRetryInterval = authorizationExceptionRetryInterval; - } - - public boolean isConsiderDownWhenAnyPartitionHasNoLeader() { - return this.considerDownWhenAnyPartitionHasNoLeader; - } - - public void setConsiderDownWhenAnyPartitionHasNoLeader(boolean considerDownWhenAnyPartitionHasNoLeader) { - this.considerDownWhenAnyPartitionHasNoLeader = considerDownWhenAnyPartitionHasNoLeader; - } - - public String getCertificateStoreDirectory() { - return this.certificateStoreDirectory; - } - - public void setCertificateStoreDirectory(String certificateStoreDirectory) { - this.certificateStoreDirectory = certificateStoreDirectory; - } - - public boolean isEnableObservation() { - return this.enableObservation; - } - - public void setEnableObservation(boolean enableObservation) { - this.enableObservation = enableObservation; - } - - /** - * Domain class that models transaction capabilities in Kafka. - */ - public static class Transaction { - - private final CombinedProducerProperties producer = new CombinedProducerProperties(); - - private String transactionIdPrefix; - - public String getTransactionIdPrefix() { - return this.transactionIdPrefix; - } - - public void setTransactionIdPrefix(String transactionIdPrefix) { - this.transactionIdPrefix = transactionIdPrefix; - } - - public CombinedProducerProperties getProducer() { - return this.producer; - } - - } - - /** - * An combination of {@link ProducerProperties} and {@link KafkaProducerProperties} so - * that common and kafka-specific properties can be set for the transactional - * producer. - * - * @since 2.1 - */ - public static class CombinedProducerProperties { - - private final ProducerProperties producerProperties = new ProducerProperties(); - - private final KafkaProducerProperties kafkaProducerProperties = new KafkaProducerProperties(); - - public Expression getPartitionKeyExpression() { - return this.producerProperties.getPartitionKeyExpression(); - } - - public void setPartitionKeyExpression(Expression partitionKeyExpression) { - this.producerProperties.setPartitionKeyExpression(partitionKeyExpression); - } - - public boolean isPartitioned() { - return this.producerProperties.isPartitioned(); - } - - public Expression getPartitionSelectorExpression() { - return this.producerProperties.getPartitionSelectorExpression(); - } - - public void setPartitionSelectorExpression( - Expression partitionSelectorExpression) { - this.producerProperties - .setPartitionSelectorExpression(partitionSelectorExpression); - } - - public @Min(value = 1, message = "Partition count should be greater than zero.") int getPartitionCount() { - return this.producerProperties.getPartitionCount(); - } - - public void setPartitionCount(int partitionCount) { - this.producerProperties.setPartitionCount(partitionCount); - } - - public String[] getRequiredGroups() { - return this.producerProperties.getRequiredGroups(); - } - - public void setRequiredGroups(String... requiredGroups) { - this.producerProperties.setRequiredGroups(requiredGroups); - } - - public @AssertTrue(message = "Partition key expression and partition key extractor class properties " - + "are mutually exclusive.") boolean isValidPartitionKeyProperty() { - return this.producerProperties.isValidPartitionKeyProperty(); - } - - public @AssertTrue(message = "Partition selector class and partition selector expression " - + "properties are mutually exclusive.") boolean isValidPartitionSelectorProperty() { - return this.producerProperties.isValidPartitionSelectorProperty(); - } - - public HeaderMode getHeaderMode() { - return this.producerProperties.getHeaderMode(); - } - - public void setHeaderMode(HeaderMode headerMode) { - this.producerProperties.setHeaderMode(headerMode); - } - - public boolean isUseNativeEncoding() { - return this.producerProperties.isUseNativeEncoding(); - } - - public void setUseNativeEncoding(boolean useNativeEncoding) { - this.producerProperties.setUseNativeEncoding(useNativeEncoding); - } - - public boolean isErrorChannelEnabled() { - return this.producerProperties.isErrorChannelEnabled(); - } - - public void setErrorChannelEnabled(boolean errorChannelEnabled) { - this.producerProperties.setErrorChannelEnabled(errorChannelEnabled); - } - - public String getPartitionKeyExtractorName() { - return this.producerProperties.getPartitionKeyExtractorName(); - } - - public void setPartitionKeyExtractorName(String partitionKeyExtractorName) { - this.producerProperties - .setPartitionKeyExtractorName(partitionKeyExtractorName); - } - - public String getPartitionSelectorName() { - return this.producerProperties.getPartitionSelectorName(); - } - - public void setPartitionSelectorName(String partitionSelectorName) { - this.producerProperties.setPartitionSelectorName(partitionSelectorName); - } - - public int getBufferSize() { - return this.kafkaProducerProperties.getBufferSize(); - } - - public void setBufferSize(int bufferSize) { - this.kafkaProducerProperties.setBufferSize(bufferSize); - } - - public @NotNull CompressionType getCompressionType() { - return this.kafkaProducerProperties.getCompressionType(); - } - - public void setCompressionType(CompressionType compressionType) { - this.kafkaProducerProperties.setCompressionType(compressionType); - } - - public boolean isSync() { - return this.kafkaProducerProperties.isSync(); - } - - public void setSync(boolean sync) { - this.kafkaProducerProperties.setSync(sync); - } - - public int getBatchTimeout() { - return this.kafkaProducerProperties.getBatchTimeout(); - } - - public void setBatchTimeout(int batchTimeout) { - this.kafkaProducerProperties.setBatchTimeout(batchTimeout); - } - - public Expression getMessageKeyExpression() { - return this.kafkaProducerProperties.getMessageKeyExpression(); - } - - public void setMessageKeyExpression(Expression messageKeyExpression) { - this.kafkaProducerProperties.setMessageKeyExpression(messageKeyExpression); - } - - public String[] getHeaderPatterns() { - return this.kafkaProducerProperties.getHeaderPatterns(); - } - - public void setHeaderPatterns(String[] headerPatterns) { - this.kafkaProducerProperties.setHeaderPatterns(headerPatterns); - } - - public Map getConfiguration() { - return this.kafkaProducerProperties.getConfiguration(); - } - - public void setConfiguration(Map configuration) { - this.kafkaProducerProperties.setConfiguration(configuration); - } - - public KafkaTopicProperties getTopic() { - return this.kafkaProducerProperties.getTopic(); - } - - public void setTopic(KafkaTopicProperties topic) { - this.kafkaProducerProperties.setTopic(topic); - } - - public KafkaProducerProperties getExtension() { - return this.kafkaProducerProperties; - } - - } - - public static class Metrics { - /** - * When set to true, the offset lag metric of each consumer topic is computed whenever the metric is queried (default: true). - * When set to false only the periodically calculated offset lag is used. - * See {@link #offsetLagMetricsInterval} for more information. - */ - private boolean defaultOffsetLagMetricsEnabled = true; - - /** - * The interval in which the offset lag for each consumer topic is computed (default: 60 seconds). - * This value is used whenever {@link #defaultOffsetLagMetricsEnabled} is disabled or its computation is taking too long. - */ - private Duration offsetLagMetricsInterval = Duration.ofSeconds(60); - - public boolean isDefaultOffsetLagMetricsEnabled() { - return defaultOffsetLagMetricsEnabled; - } - - public void setDefaultOffsetLagMetricsEnabled(boolean defaultOffsetLagMetricsEnabled) { - this.defaultOffsetLagMetricsEnabled = defaultOffsetLagMetricsEnabled; - } - - public Duration getOffsetLagMetricsInterval() { - return offsetLagMetricsInterval; - } - - public void setOffsetLagMetricsInterval(Duration offsetLagMetricsInterval) { - this.offsetLagMetricsInterval = offsetLagMetricsInterval; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBindingProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBindingProperties.java deleted file mode 100644 index e84b0b2c7..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBindingProperties.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.properties; - -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * Container object for Kafka specific extended producer and consumer binding properties. - * - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public class KafkaBindingProperties implements BinderSpecificPropertiesProvider { - - /** - * Consumer specific binding properties. @see {@link KafkaConsumerProperties}. - */ - private KafkaConsumerProperties consumer = new KafkaConsumerProperties(); - - /** - * Producer specific binding properties. @see {@link KafkaProducerProperties}. - */ - private KafkaProducerProperties producer = new KafkaProducerProperties(); - - /** - * @return {@link KafkaConsumerProperties} - * Consumer specific binding properties. @see {@link KafkaConsumerProperties}. - */ - public KafkaConsumerProperties getConsumer() { - return this.consumer; - } - - public void setConsumer(KafkaConsumerProperties consumer) { - this.consumer = consumer; - } - - /** - * @return {@link KafkaProducerProperties} - * Producer specific binding properties. @see {@link KafkaProducerProperties}. - */ - public KafkaProducerProperties getProducer() { - return this.producer; - } - - public void setProducer(KafkaProducerProperties producer) { - this.producer = producer; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaConsumerProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaConsumerProperties.java deleted file mode 100644 index d6f6205bc..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaConsumerProperties.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.properties; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.kafka.listener.ContainerProperties; - -/** - * Extended consumer properties for Kafka binder. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Soby Chacko - * @author Gary Russell - * @author Aldo Sinanaj - * - *

- * Thanks to Laszlo Szabo for providing the initial patch for generic property support. - *

- */ -public class KafkaConsumerProperties { - - /** - * Enumeration for starting consumer offset. - */ - public enum StartOffset { - - /** - * Starting from earliest offset. - */ - earliest(-2L), - /** - * Starting from latest offset. - */ - latest(-1L); - - private final long referencePoint; - - StartOffset(long referencePoint) { - this.referencePoint = referencePoint; - } - - public long getReferencePoint() { - return this.referencePoint; - } - - } - - /** - * Standard headers for the message. - */ - public enum StandardHeaders { - - /** - * No headers. - */ - none, - /** - * Message header representing ID. - */ - id, - /** - * Message header representing timestamp. - */ - timestamp, - /** - * Indicating both ID and timestamp headers. - */ - both - - } - - /** - * When true the offset is committed after each record, otherwise the offsets for the complete set of records - * received from the poll() are committed after all records have been processed. - */ - @Deprecated - private boolean ackEachRecord; - - /** - * When true, topic partitions is automatically rebalanced between the members of a consumer group. - * When false, each consumer is assigned a fixed set of partitions based on spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex. - */ - private boolean autoRebalanceEnabled = true; - - /** - * Whether to autocommit offsets when a message has been processed. - * If set to false, a header with the key kafka_acknowledgment of the type org.springframework.kafka.support.Acknowledgment header - * is present in the inbound message. Applications may use this header for acknowledging messages. - */ - @Deprecated - private boolean autoCommitOffset = true; - - /** - * Controlling the container acknowledgement mode. This is the preferred way to control the ack mode on the - * container instead of the deprecated autoCommitOffset property. - */ - private ContainerProperties.AckMode ackMode; - - /** - * Flag to enable auto commit on error in polled consumers. - */ - private Boolean autoCommitOnError; - - /** - * The starting offset for new groups. Allowed values: earliest and latest. - */ - private StartOffset startOffset; - - /** - * Whether to reset offsets on the consumer to the value provided by startOffset. - * Must be false if a KafkaRebalanceListener is provided. - */ - private boolean resetOffsets; - - /** - * When set to true, it enables DLQ behavior for the consumer. - * By default, messages that result in errors are forwarded to a topic named error.name-of-destination.name-of-group. - * The DLQ topic name can be configurable by setting the dlqName property. - */ - private boolean enableDlq; - - /** - * The name of the DLQ topic to receive the error messages. - */ - private String dlqName; - - /** - * Number of partitions to use on the DLQ. - */ - private Integer dlqPartitions; - - /** - * Using this, DLQ-specific producer properties can be set. - * All the properties available through kafka producer properties can be set through this property. - */ - private KafkaProducerProperties dlqProducerProperties = new KafkaProducerProperties(); - - /** - * @deprecated No longer used by the binder. - */ - @Deprecated - private int recoveryInterval = 5000; - - /** - * List of trusted packages to provide the header mapper. - */ - private String[] trustedPackages; - - /** - * Indicates which standard headers are populated by the inbound channel adapter. - * Allowed values: none, id, timestamp, or both. - */ - private StandardHeaders standardHeaders = StandardHeaders.none; - - /** - * The name of a bean that implements RecordMessageConverter. - */ - private String converterBeanName; - - /** - * The interval, in milliseconds, between events indicating that no messages have recently been received. - */ - private long idleEventInterval = 30_000; - - /** - * When true, the destination is treated as a regular expression Pattern used to match topic names by the broker. - */ - private boolean destinationIsPattern; - - /** - * Map with a key/value pair containing generic Kafka consumer properties. - * In addition to having Kafka consumer properties, other configuration properties can be passed here. - */ - private Map configuration = new HashMap<>(); - - /** - * Various topic level properties. @see {@link KafkaTopicProperties} for more details. - */ - private KafkaTopicProperties topic = new KafkaTopicProperties(); - - /** - * Timeout used for polling in pollable consumers. - */ - private long pollTimeout = org.springframework.kafka.listener.ConsumerProperties.DEFAULT_POLL_TIMEOUT; - - /** - * Transaction manager bean name - overrides the binder's transaction configuration. - */ - private String transactionManager; - - /** - * Set to false to NOT commit the offset of a successfully recovered recovered in the after rollback processor. - */ - private boolean txCommitRecovered = true; - - /** - * CommonErrorHandler bean name per consumer binding. - * @since 3.2 - */ - private String commonErrorHandlerBeanName; - - /** - * When using the reactive binder, automatically commit offsets when records received - * by the poll have been processed. - * @since 4.0.2 - */ - private boolean reactiveAutoCommit; - - /** - * When using the reactive binder, automatically commit offsets of records before they - * are passed to the user code. - * @since 4.0.3 - */ - private boolean reactiveAtMostOnce; - - /** - * @return if each record needs to be acknowledged. - * - * When true the offset is committed after each record, otherwise the offsets for the complete set of records - * received from the poll() are committed after all records have been processed. - * - * @deprecated since 3.1 in favor of using {@link #ackMode} - */ - @Deprecated - public boolean isAckEachRecord() { - return this.ackEachRecord; - } - - /** - * @param ackEachRecord - * - * @deprecated in favor of using {@link #ackMode} - */ - @Deprecated - public void setAckEachRecord(boolean ackEachRecord) { - this.ackEachRecord = ackEachRecord; - } - - /** - * @return is autocommit offset enabled - * - * Whether to autocommit offsets when a message has been processed. - * If set to false, a header with the key kafka_acknowledgment of the type org.springframework.kafka.support.Acknowledgment header - * is present in the inbound message. Applications may use this header for acknowledging messages. - * - * @deprecated since 3.1 in favor of using {@link #ackMode} - */ - @Deprecated - public boolean isAutoCommitOffset() { - return this.autoCommitOffset; - } - - /** - * @param autoCommitOffset - * - * @deprecated in favor of using {@link #ackMode} - */ - @Deprecated - public void setAutoCommitOffset(boolean autoCommitOffset) { - this.autoCommitOffset = autoCommitOffset; - } - - /** - * @return Container's ack mode. - */ - public ContainerProperties.AckMode getAckMode() { - return this.ackMode; - } - - public void setAckMode(ContainerProperties.AckMode ackMode) { - this.ackMode = ackMode; - } - - /** - * @return start offset - * - * The starting offset for new groups. Allowed values: earliest and latest. - */ - public StartOffset getStartOffset() { - return this.startOffset; - } - - public void setStartOffset(StartOffset startOffset) { - this.startOffset = startOffset; - } - - /** - * @return if resetting offset is enabled - * - * Whether to reset offsets on the consumer to the value provided by startOffset. - * Must be false if a KafkaRebalanceListener is provided. - */ - public boolean isResetOffsets() { - return this.resetOffsets; - } - - public void setResetOffsets(boolean resetOffsets) { - this.resetOffsets = resetOffsets; - } - - /** - * @return is DLQ enabled. - * - * When set to true, it enables DLQ behavior for the consumer. - * By default, messages that result in errors are forwarded to a topic named error.name-of-destination.name-of-group. - * The DLQ topic name can be configurable by setting the dlqName property. - */ - public boolean isEnableDlq() { - return this.enableDlq; - } - - public void setEnableDlq(boolean enableDlq) { - this.enableDlq = enableDlq; - } - - /** - * @return is autocommit on error in polled consumers. - * - * This property accessor is only used in polled consumers. - */ - public Boolean getAutoCommitOnError() { - return this.autoCommitOnError; - } - - /** - * - * @param autoCommitOnError commit on error in polled consumers. - * - */ - public void setAutoCommitOnError(Boolean autoCommitOnError) { - this.autoCommitOnError = autoCommitOnError; - } - - /** - * No longer used. - * @return the interval. - * @deprecated No longer used by the binder - */ - @Deprecated - public int getRecoveryInterval() { - return this.recoveryInterval; - } - - /** - * No longer used. - * @param recoveryInterval the interval. - * @deprecated No longer needed by the binder - */ - @Deprecated - public void setRecoveryInterval(int recoveryInterval) { - this.recoveryInterval = recoveryInterval; - } - - /** - * @return is auto rebalance enabled - * - * When true, topic partitions is automatically rebalanced between the members of a consumer group. - * When false, each consumer is assigned a fixed set of partitions based on spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex. - */ - public boolean isAutoRebalanceEnabled() { - return this.autoRebalanceEnabled; - } - - public void setAutoRebalanceEnabled(boolean autoRebalanceEnabled) { - this.autoRebalanceEnabled = autoRebalanceEnabled; - } - - /** - * @return a map of configuration - * - * Map with a key/value pair containing generic Kafka consumer properties. - * In addition to having Kafka consumer properties, other configuration properties can be passed here. - */ - public Map getConfiguration() { - return this.configuration; - } - - public void setConfiguration(Map configuration) { - this.configuration = configuration; - } - - /** - * @return dlq name - * - * The name of the DLQ topic to receive the error messages. - */ - public String getDlqName() { - return this.dlqName; - } - - public void setDlqName(String dlqName) { - this.dlqName = dlqName; - } - - /** - * @return number of partitions on the DLQ topic - * - * Number of partitions to use on the DLQ. - */ - public Integer getDlqPartitions() { - return this.dlqPartitions; - } - - public void setDlqPartitions(Integer dlqPartitions) { - this.dlqPartitions = dlqPartitions; - } - - /** - * @return trusted packages - * - * List of trusted packages to provide the header mapper. - */ - public String[] getTrustedPackages() { - return this.trustedPackages; - } - - public void setTrustedPackages(String[] trustedPackages) { - this.trustedPackages = trustedPackages; - } - - /** - * @return dlq producer properties - * - * Using this, DLQ-specific producer properties can be set. - * All the properties available through kafka producer properties can be set through this property. - */ - public KafkaProducerProperties getDlqProducerProperties() { - return this.dlqProducerProperties; - } - - public void setDlqProducerProperties(KafkaProducerProperties dlqProducerProperties) { - this.dlqProducerProperties = dlqProducerProperties; - } - - /** - * @return standard headers - * - * Indicates which standard headers are populated by the inbound channel adapter. - * Allowed values: none, id, timestamp, or both. - */ - public StandardHeaders getStandardHeaders() { - return this.standardHeaders; - } - - public void setStandardHeaders(StandardHeaders standardHeaders) { - this.standardHeaders = standardHeaders; - } - - /** - * @return converter bean name - * - * The name of a bean that implements RecordMessageConverter. - */ - public String getConverterBeanName() { - return this.converterBeanName; - } - - public void setConverterBeanName(String converterBeanName) { - this.converterBeanName = converterBeanName; - } - - /** - * @return idle event interval - * - * The interval, in milliseconds, between events indicating that no messages have recently been received. - */ - public long getIdleEventInterval() { - return this.idleEventInterval; - } - - public void setIdleEventInterval(long idleEventInterval) { - this.idleEventInterval = idleEventInterval; - } - - /** - * @return is destination given through a pattern - * - * When true, the destination is treated as a regular expression Pattern used to match topic names by the broker. - */ - public boolean isDestinationIsPattern() { - return this.destinationIsPattern; - } - - public void setDestinationIsPattern(boolean destinationIsPattern) { - this.destinationIsPattern = destinationIsPattern; - } - - /** - * @return topic properties - * - * Various topic level properties. @see {@link KafkaTopicProperties} for more details. - */ - public KafkaTopicProperties getTopic() { - return this.topic; - } - - public void setTopic(KafkaTopicProperties topic) { - this.topic = topic; - } - - /** - * @return timeout in pollable consumers - * - * Timeout used for polling in pollable consumers. - */ - public long getPollTimeout() { - return this.pollTimeout; - } - - public void setPollTimeout(long pollTimeout) { - this.pollTimeout = pollTimeout; - } - - /** - * @return the transaction manager bean name. - * - * Transaction manager bean name (must be {@code KafkaAwareTransactionManager}. - */ - public String getTransactionManager() { - return this.transactionManager; - } - - public void setTransactionManager(String transactionManager) { - this.transactionManager = transactionManager; - } - - public boolean isTxCommitRecovered() { - return this.txCommitRecovered; - } - - public void setTxCommitRecovered(boolean txCommitRecovered) { - this.txCommitRecovered = txCommitRecovered; - } - - public String getCommonErrorHandlerBeanName() { - return commonErrorHandlerBeanName; - } - - public void setCommonErrorHandlerBeanName(String commonErrorHandlerBeanName) { - this.commonErrorHandlerBeanName = commonErrorHandlerBeanName; - } - - public boolean isReactiveAutoCommit() { - return this.reactiveAutoCommit; - } - - public void setReactiveAutoCommit(boolean reactiveAutoCommit) { - this.reactiveAutoCommit = reactiveAutoCommit; - } - - public boolean isReactiveAtMostOnce() { - return this.reactiveAtMostOnce; - } - - public void setReactiveAtMostOnce(boolean reactiveAtMostOnce) { - this.reactiveAtMostOnce = reactiveAtMostOnce; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaExtendedBindingProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaExtendedBindingProperties.java deleted file mode 100644 index 99357cc2a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaExtendedBindingProperties.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.properties; - -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * Kafka specific extended binding properties class that extends from - * {@link AbstractExtendedBindingProperties}. - * - * @author Marius Bogoevici - * @author Gary Russell - * @author Soby Chacko - * @author Oleg Zhurakousky - */ -@ConfigurationProperties("spring.cloud.stream.kafka") -public class KafkaExtendedBindingProperties extends - AbstractExtendedBindingProperties { - - private static final String DEFAULTS_PREFIX = "spring.cloud.stream.kafka.default"; - - @Override - public String getDefaultsPrefix() { - return DEFAULTS_PREFIX; - } - - @Override - public Map getBindings() { - return this.doGetBindings(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return KafkaBindingProperties.class; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaProducerProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaProducerProperties.java deleted file mode 100644 index a64d08dda..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaProducerProperties.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.properties; - -import java.util.HashMap; -import java.util.Map; - -import jakarta.validation.constraints.NotNull; - -import org.springframework.expression.Expression; - -/** - * Extended producer properties for Kafka binder. - * - * @author Marius Bogoevici - * @author Henryk Konsek - * @author Gary Russell - * @author Aldo Sinanaj - */ -public class KafkaProducerProperties { - - /** - * Upper limit, in bytes, of how much data the Kafka producer attempts to batch before sending. - */ - private int bufferSize = 16384; - - /** - * Set the compression.type producer property. Supported values are none, gzip, snappy and lz4. - * See {@link CompressionType} for more details. - */ - private CompressionType compressionType = CompressionType.none; - - /** - * Whether the producer is synchronous. - */ - private boolean sync; - - /** - * A SpEL expression evaluated against the outgoing message used to evaluate the time to wait - * for ack when synchronous publish is enabled. - */ - private Expression sendTimeoutExpression; - - /** - * How long the producer waits to allow more messages to accumulate in the same batch before sending the messages. - */ - private int batchTimeout; - - /** - * A SpEL expression evaluated against the outgoing message used to populate the key of the produced Kafka message. - */ - private Expression messageKeyExpression; - - /** - * A comma-delimited list of simple patterns to match Spring messaging headers - * to be mapped to the Kafka Headers in the ProducerRecord. - */ - private String[] headerPatterns; - - /** - * Map with a key/value pair containing generic Kafka producer properties. - */ - private Map configuration = new HashMap<>(); - - /** - * Various topic level properties. @see {@link KafkaTopicProperties} for more details. - */ - private KafkaTopicProperties topic = new KafkaTopicProperties(); - - /** - * Set to true to override the default binding destination (topic name) with the value of the - * KafkaHeaders.TOPIC message header in the outbound message. If the header is not present, - * the default binding destination is used. - */ - private boolean useTopicHeader; - - /** - * The bean name of a MessageChannel to which successful send results should be sent; - * the bean must exist in the application context. When using the reactive binder, - * the channel must be of type FluxMessageChannel. - */ - private String recordMetadataChannel; - - /** - * Transaction manager bean name - overrides the binder's transaction configuration. - */ - private String transactionManager; - - /* - * Timeout value in seconds for the duration to wait when closing the producer. - * If not set this defaults to 30 seconds. - */ - private int closeTimeout; - - /** - * Set to true to disable transactions. - */ - private boolean allowNonTransactional; - - /** - * @return buffer size - * - * Upper limit, in bytes, of how much data the Kafka producer attempts to batch before sending. - */ - public int getBufferSize() { - return this.bufferSize; - } - - public void setBufferSize(int bufferSize) { - this.bufferSize = bufferSize; - } - - /** - * @return compression type {@link CompressionType} - * - * Set the compression.type producer property. Supported values are none, gzip, snappy, lz4 and zstd. - * See {@link CompressionType} for more details. - */ - @NotNull - public CompressionType getCompressionType() { - return this.compressionType; - } - - public void setCompressionType(CompressionType compressionType) { - this.compressionType = compressionType; - } - - /** - * @return if synchronous sending is enabled - * - * Whether the producer is synchronous. - */ - public boolean isSync() { - return this.sync; - } - - public void setSync(boolean sync) { - this.sync = sync; - } - - /** - * @return timeout expression for send - * - * A SpEL expression evaluated against the outgoing message used to evaluate the time to wait - * for ack when synchronous publish is enabled. - */ - public Expression getSendTimeoutExpression() { - return this.sendTimeoutExpression; - } - - public void setSendTimeoutExpression(Expression sendTimeoutExpression) { - this.sendTimeoutExpression = sendTimeoutExpression; - } - - /** - * @return batch timeout - * - * How long the producer waits to allow more messages to accumulate in the same batch before sending the messages. - */ - public int getBatchTimeout() { - return this.batchTimeout; - } - - public void setBatchTimeout(int batchTimeout) { - this.batchTimeout = batchTimeout; - } - - /** - * @return message key expression - * - * A SpEL expression evaluated against the outgoing message used to populate the key of the produced Kafka message. - */ - public Expression getMessageKeyExpression() { - return this.messageKeyExpression; - } - - public void setMessageKeyExpression(Expression messageKeyExpression) { - this.messageKeyExpression = messageKeyExpression; - } - - /** - * @return header patterns - * - * A comma-delimited list of simple patterns to match Spring messaging headers - * to be mapped to the Kafka Headers in the ProducerRecord. - */ - public String[] getHeaderPatterns() { - return this.headerPatterns; - } - - public void setHeaderPatterns(String[] headerPatterns) { - this.headerPatterns = headerPatterns; - } - - /** - * @return map of configuration - * - * Map with a key/value pair containing generic Kafka producer properties. - */ - public Map getConfiguration() { - return this.configuration; - } - - public void setConfiguration(Map configuration) { - this.configuration = configuration; - } - - /** - * @return topic properties - * - * Various topic level properties. @see {@link KafkaTopicProperties} for more details. - */ - public KafkaTopicProperties getTopic() { - return this.topic; - } - - public void setTopic(KafkaTopicProperties topic) { - this.topic = topic; - } - - /** - * @return if using topic header - * - * Set to true to override the default binding destination (topic name) with the value of the - * KafkaHeaders.TOPIC message header in the outbound message. If the header is not present, - * the default binding destination is used. - */ - public boolean isUseTopicHeader() { - return this.useTopicHeader; - } - - public void setUseTopicHeader(boolean useTopicHeader) { - this.useTopicHeader = useTopicHeader; - } - - /** - * @return record metadata channel - * - * The bean name of a MessageChannel to which successful send results should be sent; - * the bean must exist in the application context. - */ - public String getRecordMetadataChannel() { - return this.recordMetadataChannel; - } - - public void setRecordMetadataChannel(String recordMetadataChannel) { - this.recordMetadataChannel = recordMetadataChannel; - } - - /** - * @return the transaction manager bean name. - * - * Transaction manager bean name (must be {@code KafkaAwareTransactionManager}. - */ - public String getTransactionManager() { - return this.transactionManager; - } - - public void setTransactionManager(String transactionManager) { - this.transactionManager = transactionManager; - } - - /* - * @return timeout in seconds for closing the producer - */ - public int getCloseTimeout() { - return this.closeTimeout; - } - - public void setCloseTimeout(int closeTimeout) { - this.closeTimeout = closeTimeout; - } - - public boolean isAllowNonTransactional() { - return this.allowNonTransactional; - } - - public void setAllowNonTransactional(boolean allowNonTransactional) { - this.allowNonTransactional = allowNonTransactional; - } - - /** - * Enumeration for compression types. - */ - public enum CompressionType { - - /** - * No compression. - */ - none, - - /** - * gzip based compression. - */ - gzip, - - /** - * snappy based compression. - */ - snappy, - - /** - * lz4 compression. - */ - lz4, - - /** - * zstd compression. - */ - zstd, - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaTopicProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaTopicProperties.java deleted file mode 100644 index e9a1425b7..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaTopicProperties.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.properties; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Properties for configuring topics. - * - * @author Aldo Sinanaj - * @since 2.2 - * - */ -public class KafkaTopicProperties { - - private Short replicationFactor; - - private Map> replicasAssignments = new HashMap<>(); - - private Map properties = new HashMap<>(); - - public Short getReplicationFactor() { - return replicationFactor; - } - - public void setReplicationFactor(Short replicationFactor) { - this.replicationFactor = replicationFactor; - } - - public Map> getReplicasAssignments() { - return replicasAssignments; - } - - public void setReplicasAssignments(Map> replicasAssignments) { - this.replicasAssignments = replicasAssignments; - } - - public Map getProperties() { - return properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/provisioning/AdminClientConfigCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/provisioning/AdminClientConfigCustomizer.java deleted file mode 100644 index 25b2fe79e..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/provisioning/AdminClientConfigCustomizer.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.provisioning; - -import java.util.Map; - -/** - * Customizer for configuring AdminClient. - * - * @author Soby Chacko - * @since 3.1.2 - */ -@FunctionalInterface -public interface AdminClientConfigCustomizer { - - void configure(Map adminClientProperties); -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/provisioning/KafkaTopicProvisioner.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/provisioning/KafkaTopicProvisioner.java deleted file mode 100644 index f9e823e9f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/provisioning/KafkaTopicProvisioner.java +++ /dev/null @@ -1,690 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.provisioning; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.CommonClientConfigs; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.AlterConfigOp; -import org.apache.kafka.clients.admin.AlterConfigsResult; -import org.apache.kafka.clients.admin.Config; -import org.apache.kafka.clients.admin.ConfigEntry; -import org.apache.kafka.clients.admin.CreatePartitionsResult; -import org.apache.kafka.clients.admin.CreateTopicsResult; -import org.apache.kafka.clients.admin.DescribeConfigsResult; -import org.apache.kafka.clients.admin.DescribeTopicsResult; -import org.apache.kafka.clients.admin.ListTopicsResult; -import org.apache.kafka.clients.admin.NewPartitions; -import org.apache.kafka.clients.admin.NewTopic; -import org.apache.kafka.clients.admin.TopicDescription; -import org.apache.kafka.common.KafkaFuture; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.config.ConfigResource; -import org.apache.kafka.common.errors.TopicExistsException; -import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.BinderException; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaTopicProperties; -import org.springframework.cloud.stream.binder.kafka.utils.KafkaTopicUtils; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.cloud.stream.provisioning.ProvisioningException; -import org.springframework.cloud.stream.provisioning.ProvisioningProvider; -import org.springframework.retry.RetryOperations; -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.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * Kafka implementation for {@link ProvisioningProvider}. - * - * @author Soby Chacko - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @author Simon Flandergan - * @author Oleg Zhurakousky - * @author Aldo Sinanaj - * @author Yi Liu - * @author Omer Celik - * @author Byungjun You - */ -public class KafkaTopicProvisioner implements - // @checkstyle:off - ProvisioningProvider, ExtendedProducerProperties>, - // @checkstyle:on - InitializingBean { - - private static final Log logger = LogFactory.getLog(KafkaTopicProvisioner.class); - - private static final int DEFAULT_OPERATION_TIMEOUT = 30; - - private final KafkaBinderConfigurationProperties configurationProperties; - - private final int operationTimeout = DEFAULT_OPERATION_TIMEOUT; - - private final Map adminClientProperties; - - private RetryOperations metadataRetryOperations; - - /** - * Create an instance. - * @param kafkaBinderConfigurationProperties the binder configuration properties. - * @param kafkaProperties the boot Kafka properties used to build the - * @param adminClientConfigCustomizer to customize {@link AdminClient}. - * {@link AdminClient}. - */ - public KafkaTopicProvisioner( - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties, - KafkaProperties kafkaProperties, - AdminClientConfigCustomizer adminClientConfigCustomizer) { - this(kafkaBinderConfigurationProperties, kafkaProperties, adminClientConfigCustomizer != null ? - Arrays.asList(adminClientConfigCustomizer) : new ArrayList<>()); - } - - /** - * Create an instance. - * - * @param kafkaBinderConfigurationProperties the binder configuration properties. - * @param kafkaProperties the boot Kafka properties used to build the - * @param adminClientConfigCustomizers to customize {@link AdminClient}. - * {@link AdminClient}. - */ - public KafkaTopicProvisioner( - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties, - KafkaProperties kafkaProperties, - List adminClientConfigCustomizers) { - - Assert.isTrue(kafkaProperties != null, "KafkaProperties cannot be null"); - this.configurationProperties = kafkaBinderConfigurationProperties; - this.adminClientProperties = kafkaProperties.buildAdminProperties(); - normalalizeBootPropsWithBinder(this.adminClientProperties, kafkaProperties, - kafkaBinderConfigurationProperties); - // If the application provides AdminConfig customizers - // and overrides properties, those take precedence. - adminClientConfigCustomizers.forEach(customizer -> customizer.configure(this.adminClientProperties)); - } - - /** - * Return an unmodifiable map of merged admin properties. - * @return the properties. - * @since 4.0.3 - */ - public Map getAdminClientProperties() { - return Collections.unmodifiableMap(this.adminClientProperties); - } - - /** - * Mutator for metadata retry operations. - * @param metadataRetryOperations the retry configuration - */ - public void setMetadataRetryOperations(RetryOperations metadataRetryOperations) { - this.metadataRetryOperations = metadataRetryOperations; - } - - @Override - public void afterPropertiesSet() { - if (this.metadataRetryOperations == null) { - RetryTemplate retryTemplate = new RetryTemplate(); - - SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(); - simpleRetryPolicy.setMaxAttempts(10); - retryTemplate.setRetryPolicy(simpleRetryPolicy); - - ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); - backOffPolicy.setInitialInterval(100); - backOffPolicy.setMultiplier(2); - backOffPolicy.setMaxInterval(1000); - retryTemplate.setBackOffPolicy(backOffPolicy); - this.metadataRetryOperations = retryTemplate; - } - } - - @Override - public ProducerDestination provisionProducerDestination(final String name, - ExtendedProducerProperties properties) { - - if (logger.isInfoEnabled()) { - logger.info("Using kafka topic for outbound: " + name); - } - if (this.configurationProperties.isAutoCreateTopics()) { - KafkaTopicUtils.validateTopicName(name); - try (AdminClient adminClient = createAdminClient()) { - createTopic(adminClient, name, properties.getPartitionCount(), false, - properties.getExtension().getTopic()); - int partitions = getPartitionsForTopic(name, adminClient); - return new KafkaProducerDestination(name, partitions); - } - } - return new KafkaProducerDestination(name, 0); - } - - @Override - public ConsumerDestination provisionConsumerDestination(final String name, - final String group, - ExtendedConsumerProperties properties) { - if (!properties.isMultiplex()) { - return doProvisionConsumerDestination(name, group, properties); - } - else { - String[] destinations = StringUtils.commaDelimitedListToStringArray(name); - for (String destination : destinations) { - doProvisionConsumerDestination(destination.trim(), group, properties); - } - return new KafkaConsumerDestination(name); - } - } - - private ConsumerDestination doProvisionConsumerDestination(final String name, - final String group, - ExtendedConsumerProperties properties) { - final KafkaConsumerDestination kafkaConsumerDestination = new KafkaConsumerDestination(name); - if (properties.getExtension().isDestinationIsPattern()) { - Assert.isTrue(!properties.getExtension().isEnableDlq(), - "enableDLQ is not allowed when listening to topic patterns"); - if (logger.isDebugEnabled()) { - logger.debug("Listening to a topic pattern - " + name - + " - no provisioning performed"); - } - return kafkaConsumerDestination; - } - if (this.configurationProperties.isAutoCreateTopics()) { - KafkaTopicUtils.validateTopicName(name); - boolean anonymous = !StringUtils.hasText(group); - Assert.isTrue(!anonymous || !properties.getExtension().isEnableDlq(), - "DLQ support is not available for anonymous subscriptions"); - if (properties.getInstanceCount() == 0) { - throw new IllegalArgumentException("Instance count cannot be zero"); - } - int partitionCount = properties.getInstanceCount() * properties.getConcurrency(); - ConsumerDestination consumerDestination; - try (AdminClient adminClient = createAdminClient()) { - createTopic(adminClient, name, partitionCount, - properties.getExtension().isAutoRebalanceEnabled(), - properties.getExtension().getTopic()); - int partitions = getPartitionsForTopic(name, adminClient); - consumerDestination = createDlqIfNeedBe(adminClient, name, group, - properties, anonymous, partitions); - if (consumerDestination == null) { - consumerDestination = new KafkaConsumerDestination(name, - partitions); - } - return consumerDestination; - } - } - return kafkaConsumerDestination; - } - private int getPartitionsForTopic(String topicName, AdminClient adminClient) { - int partitions = 0; - Map topicDescriptions = retrieveTopicDescriptions(topicName, adminClient); - TopicDescription topicDescription = topicDescriptions.get(topicName); - if (topicDescription != null) { - partitions = topicDescription.partitions().size(); - } - return partitions; - } - - private Map retrieveTopicDescriptions(String topicName, AdminClient adminClient) { - return this.metadataRetryOperations.execute(context -> { - try { - if (logger.isDebugEnabled()) { - logger.debug("Attempting to retrieve the description for the topic: " + topicName); - } - DescribeTopicsResult describeTopicsResult = adminClient - .describeTopics(Collections.singletonList(topicName)); - KafkaFuture> all = describeTopicsResult - .allTopicNames(); - return all.get(this.operationTimeout, TimeUnit.SECONDS); - } - catch (Exception ex) { - throw new ProvisioningException("Problems encountered with partitions finding for: " + topicName, ex); - } - }); - } - - AdminClient createAdminClient() { - return AdminClient.create(this.adminClientProperties); - } - - /** - * In general, binder properties supersede boot kafka properties. The one exception is - * the bootstrap servers. In that case, we should only override the boot properties if - * (there is a binder property AND it is a non-default value) OR (if there is no boot - * property); this is needed because the binder property never returns a null value. - * @param adminProps the admin properties to normalize. - * @param bootProps the boot kafka properties. - * @param binderProps the binder kafka properties. - */ - public static void normalalizeBootPropsWithBinder(Map adminProps, - KafkaProperties bootProps, KafkaBinderConfigurationProperties binderProps) { - // First deal with the outlier - String kafkaConnectionString = binderProps.getKafkaConnectionString(); - if (ObjectUtils - .isEmpty(adminProps.get(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG)) - || !kafkaConnectionString - .equals(binderProps.getDefaultKafkaConnectionString())) { - adminProps.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, - kafkaConnectionString); - } - // Now override any boot values with binder values - Map binderProperties = binderProps.getConfiguration(); - Set adminConfigNames = AdminClientConfig.configNames(); - binderProperties.forEach((key, value) -> { - if (key.equals(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG)) { - throw new IllegalStateException( - "Set binder bootstrap servers via the 'brokers' property, not 'configuration'"); - } - if (adminConfigNames.contains(key)) { - Object replaced = adminProps.put(key, value); - if (replaced != null && KafkaTopicProvisioner.logger.isDebugEnabled()) { - KafkaTopicProvisioner.logger.debug("Overrode boot property: [" + key + "], from: [" - + replaced + "] to: [" + value + "]"); - } - } - }); - } - - private ConsumerDestination createDlqIfNeedBe(AdminClient adminClient, String name, - String group, ExtendedConsumerProperties properties, - boolean anonymous, int partitions) { - - if (properties.getExtension().isEnableDlq() && !anonymous) { - String dlqTopic = StringUtils.hasText(properties.getExtension().getDlqName()) - ? properties.getExtension().getDlqName() - : "error." + name + "." + group; - int dlqPartitions = properties.getExtension().getDlqPartitions() == null - ? partitions - : properties.getExtension().getDlqPartitions(); - try { - final KafkaProducerProperties dlqProducerProperties = properties.getExtension().getDlqProducerProperties(); - createTopicAndPartitions(adminClient, dlqTopic, dlqPartitions, - properties.getExtension().isAutoRebalanceEnabled(), - dlqProducerProperties.getTopic()); - } - catch (Throwable throwable) { - if (throwable instanceof Error throwableError) { - throw throwableError; - } - else { - throw new ProvisioningException("Provisioning exception encountered for " + name, throwable); - } - } - return new KafkaConsumerDestination(name, partitions, dlqTopic); - } - return null; - } - - private void createTopic(AdminClient adminClient, String name, int partitionCount, - boolean tolerateLowerPartitionsOnBroker, KafkaTopicProperties properties) { - try { - createTopicIfNecessary(adminClient, name, partitionCount, - tolerateLowerPartitionsOnBroker, properties); - } - // TODO: Remove catching Throwable. See this thread: - // TODO: - // https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/pull/514#discussion_r241075940 - catch (Throwable throwable) { - if (throwable instanceof Error throwableError) { - throw throwableError; - } - else { - // TODO: - // https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/pull/514#discussion_r241075940 - throw new ProvisioningException("Provisioning exception encountered for " + name, throwable); - } - } - } - - private void createTopicIfNecessary(AdminClient adminClient, final String topicName, - final int partitionCount, boolean tolerateLowerPartitionsOnBroker, - KafkaTopicProperties properties) throws Throwable { - - if (this.configurationProperties.isAutoCreateTopics()) { - createTopicAndPartitions(adminClient, topicName, partitionCount, - tolerateLowerPartitionsOnBroker, properties); - } - else { - logger.info("Auto creation of topics is disabled."); - } - } - - /** - * Creates a Kafka topic if needed, or try to increase its partition count to the - * desired number. - * @param adminClient kafka admin client - * @param topicName topic name - * @param partitionCount partition count - * @param tolerateLowerPartitionsOnBroker whether lower partitions count on broker is - * tolerated ot not - * @param topicProperties kafka topic properties - * @throws Throwable from topic creation - */ - private void createTopicAndPartitions(AdminClient adminClient, final String topicName, - final int partitionCount, boolean tolerateLowerPartitionsOnBroker, - KafkaTopicProperties topicProperties) throws Throwable { - - ListTopicsResult listTopicsResult = adminClient.listTopics(); - KafkaFuture> namesFutures = listTopicsResult.names(); - - Set names = namesFutures.get(this.operationTimeout, TimeUnit.SECONDS); - if (names.contains(topicName)) { - //check if topic.properties are different from Topic Configuration in Kafka - if (this.configurationProperties.isAutoAlterTopics()) { - alterTopicConfigsIfNecessary(adminClient, topicName, topicProperties); - } - // only consider minPartitionCount for resizing if autoAddPartitions is true - int effectivePartitionCount = this.configurationProperties - .isAutoAddPartitions() - ? Math.max( - this.configurationProperties.getMinPartitionCount(), - partitionCount) - : partitionCount; - DescribeTopicsResult describeTopicsResult = adminClient - .describeTopics(Collections.singletonList(topicName)); - KafkaFuture> topicDescriptionsFuture = describeTopicsResult - .all(); - Map topicDescriptions = topicDescriptionsFuture - .get(this.operationTimeout, TimeUnit.SECONDS); - TopicDescription topicDescription = topicDescriptions.get(topicName); - int partitionSize = topicDescription.partitions().size(); - if (partitionSize < effectivePartitionCount) { - if (this.configurationProperties.isAutoAddPartitions()) { - CreatePartitionsResult partitions = adminClient - .createPartitions(Collections.singletonMap(topicName, - NewPartitions.increaseTo(effectivePartitionCount))); - partitions.all().get(this.operationTimeout, TimeUnit.SECONDS); - } - else if (tolerateLowerPartitionsOnBroker) { - logger.warn("The number of expected partitions for topic " - + topicName + " was: " - + partitionCount + ", but " + partitionSize - + (partitionSize > 1 ? " have " : " has ") - + "been found instead. " + "There will be " - + (effectivePartitionCount - partitionSize) - + " idle consumers"); - } - else { - throw new ProvisioningException( - "The number of expected partitions for topic " + topicName - + " was: " + partitionCount - + ", but " + partitionSize - + (partitionSize > 1 ? " have " : " has ") - + "been found instead. " - + "Consider either increasing the partition count of the topic or enabling " - + "`autoAddPartitions`"); - } - } - } - else { - // always consider minPartitionCount for topic creation - final int effectivePartitionCount = Math.max( - this.configurationProperties.getMinPartitionCount(), partitionCount); - this.metadataRetryOperations.execute((context) -> { - - NewTopic newTopic; - Map> replicasAssignments = topicProperties - .getReplicasAssignments(); - if (replicasAssignments != null && replicasAssignments.size() > 0) { - newTopic = new NewTopic(topicName, - topicProperties.getReplicasAssignments()); - } - else { - newTopic = new NewTopic(topicName, effectivePartitionCount, - topicProperties.getReplicationFactor() != null - ? topicProperties.getReplicationFactor() - : this.configurationProperties - .getReplicationFactor()); - } - if (topicProperties.getProperties().size() > 0) { - newTopic.configs(topicProperties.getProperties()); - } - CreateTopicsResult createTopicsResult = adminClient - .createTopics(Collections.singletonList(newTopic)); - try { - createTopicsResult.all().get(this.operationTimeout, TimeUnit.SECONDS); - } - catch (Exception ex) { - if (ex instanceof ExecutionException) { - if (ex.getCause() instanceof TopicExistsException) { - if (logger.isWarnEnabled()) { - logger.warn("Attempt to create topic: " + topicName - + ". Topic already exists."); - } - } - else { - logger.error("Failed to create topics", ex.getCause()); - throw ex.getCause(); - } - } - else { - logger.error("Failed to create topics", ex.getCause()); - throw ex.getCause(); - } - } - return null; - }); - } - } - - private void alterTopicConfigsIfNecessary(AdminClient adminClient, - String topicName, - KafkaTopicProperties topicProperties) - throws InterruptedException, ExecutionException, java.util.concurrent.TimeoutException { - ConfigResource topicConfigResource = new ConfigResource(ConfigResource.Type.TOPIC, topicName); - DescribeConfigsResult describeConfigsResult = adminClient - .describeConfigs(Collections.singletonList(topicConfigResource)); - KafkaFuture> topicConfigurationFuture = describeConfigsResult.all(); - Map topicConfigMap = topicConfigurationFuture - .get(this.operationTimeout, TimeUnit.SECONDS); - Config config = topicConfigMap.get(topicConfigResource); - final List updatedConfigEntries = topicProperties.getProperties().entrySet().stream() - .filter(propertiesEntry -> { - // Property is new and should be added - if (config.get(propertiesEntry.getKey()) == null) { - return true; - } - else { - // Property changed and should be updated - return !config.get(propertiesEntry.getKey()).value().equals(propertiesEntry.getValue()); - } - - }) - .map(propertyEntry -> new ConfigEntry(propertyEntry.getKey(), propertyEntry.getValue())) - .map(configEntry -> new AlterConfigOp(configEntry, AlterConfigOp.OpType.SET)) - .collect(Collectors.toList()); - if (!updatedConfigEntries.isEmpty()) { - if (logger.isDebugEnabled()) { - logger.debug("Attempting to alter configs " + updatedConfigEntries + " for the topic:" + topicName); - } - Map> alterConfigForTopics = new HashMap<>(); - alterConfigForTopics.put(topicConfigResource, updatedConfigEntries); - AlterConfigsResult alterConfigsResult = adminClient.incrementalAlterConfigs(alterConfigForTopics); - alterConfigsResult.all().get(this.operationTimeout, TimeUnit.SECONDS); - } - } - - /** - * Check that the topic has the expected number of partitions and return the partition information. - * @param partitionCount the expected count. - * @param tolerateLowerPartitionsOnBroker if false, throw an exception if there are not enough partitions. - * @param callable a Callable that will provide the partition information. - * @param topicName the topic./ - * @return the partition information. - */ - public Collection getPartitionsForTopic(final int partitionCount, - final boolean tolerateLowerPartitionsOnBroker, - final Callable> callable, final String topicName) { - try { - return this.metadataRetryOperations.execute((context) -> { - Collection partitions = Collections.emptyList(); - - try { - // This call may return null or throw an exception. - partitions = callable.call(); - } - catch (Exception ex) { - // The above call can potentially throw exceptions such as timeout. If - // we can determine - // that the exception was due to an unknown topic on the broker, just - // simply rethrow that. - if (ex instanceof UnknownTopicOrPartitionException) { - throw ex; - } - logger.error("Failed to obtain partition information for the topic " - + "(" + topicName + ").", ex); - } - // In some cases, the above partition query may not throw an UnknownTopic..Exception for various reasons. - // For that, we are forcing another query to ensure that the topic is present on the server. - if (CollectionUtils.isEmpty(partitions)) { - try (AdminClient adminClient = createAdminClient()) { - final DescribeTopicsResult describeTopicsResult = adminClient - .describeTopics(Collections.singletonList(topicName)); - - describeTopicsResult.all().get(); - } - catch (ExecutionException ex) { - if (ex.getCause() instanceof UnknownTopicOrPartitionException unknownTopicOrPartitionException) { - throw unknownTopicOrPartitionException; - } - else { - logger.warn("No partitions have been retrieved for the topic " - + "(" + topicName - + "). This will affect the health check."); - } - } - } - // do a sanity check on the partition set - int partitionSize = CollectionUtils.isEmpty(partitions) ? 0 : partitions.size(); - if (partitionSize < partitionCount) { - if (tolerateLowerPartitionsOnBroker) { - logger.warn("The number of expected partitions for topic " - + topicName + " was: " - + partitionCount + ", but " + partitionSize - + (partitionSize > 1 ? " have " : " has ") - + "been found instead. " + "There will be " - + (partitionCount - partitionSize) + " idle consumers"); - } - else { - throw new IllegalStateException( - "The number of expected partitions for topic " + topicName - + " was: " + partitionCount - + ", but " + partitionSize - + (partitionSize > 1 ? " have " : " has ") - + "been found instead"); - } - } - return partitions; - }); - } - catch (Exception ex) { - logger.error("Cannot initialize Binder checking the topic (" + topicName + ").", ex); - throw new BinderException("Cannot initialize binder checking the topic (" + topicName + "):", ex); - } - } - - private static final class KafkaProducerDestination implements ProducerDestination { - - private final String producerDestinationName; - - private final int partitions; - - KafkaProducerDestination(String destinationName, Integer partitions) { - this.producerDestinationName = destinationName; - this.partitions = partitions; - } - - @Override - public String getName() { - return this.producerDestinationName; - } - - @Override - public String getNameForPartition(int partition) { - return this.producerDestinationName; - } - - @Override - public String toString() { - return "KafkaProducerDestination{" + "producerDestinationName='" - + producerDestinationName + '\'' + ", partitions=" + partitions + '}'; - } - - } - - private static final class KafkaConsumerDestination implements ConsumerDestination { - - private final String consumerDestinationName; - - private final int partitions; - - private final String dlqName; - - KafkaConsumerDestination(String consumerDestinationName) { - this(consumerDestinationName, 0, null); - } - - KafkaConsumerDestination(String consumerDestinationName, int partitions) { - this(consumerDestinationName, partitions, null); - } - - KafkaConsumerDestination(String consumerDestinationName, Integer partitions, - String dlqName) { - this.consumerDestinationName = consumerDestinationName; - this.partitions = partitions; - this.dlqName = dlqName; - } - - @Override - public String getName() { - return this.consumerDestinationName; - } - - @Override - public String toString() { - return "KafkaConsumerDestination{" + "consumerDestinationName='" - + consumerDestinationName + '\'' + ", partitions=" + partitions - + ", dlqName='" + dlqName + '\'' + '}'; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/support/ConsumerConfigCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/support/ConsumerConfigCustomizer.java deleted file mode 100644 index 135d85d51..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/support/ConsumerConfigCustomizer.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.support; - -import java.util.Map; - -/** - * This customizer is called by the binder to customize consumer configuration in - * Kafka Consumer factory. - * - * @author Soby Chacko - * @since 3.0.9 - */ -@FunctionalInterface -public interface ConsumerConfigCustomizer { - - void configure(Map consumerProperties, String bindingName, String destination); - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/support/ProducerConfigCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/support/ProducerConfigCustomizer.java deleted file mode 100644 index 9a260cdf4..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/support/ProducerConfigCustomizer.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.support; - -import java.util.Map; - -/** - * This customizer is called by the binder to customize producer configuration in - * Kafka Producer factory. - * - * @author Soby Chacko - * @since 3.0.9 - */ -@FunctionalInterface -public interface ProducerConfigCustomizer { - - void configure(Map producerProperties, String bindingName, String destination); - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/BindingUtils.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/BindingUtils.java deleted file mode 100644 index e9a45fb90..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/BindingUtils.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.utils; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.apache.kafka.common.serialization.ByteArraySerializer; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties.StandardHeaders; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.kafka.support.DefaultKafkaHeaderMapper; -import org.springframework.kafka.support.KafkaHeaderMapper; -import org.springframework.kafka.support.converter.MessageConverter; -import org.springframework.kafka.support.converter.MessagingMessageConverter; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - -/** - * Binding Utilities. - * - * @author Gary Russell - * @since 4.0 - * - */ -public final class BindingUtils { - - private BindingUtils() { - } - - /** - * Get the message converter for consumer bindings from the application context. If - * the binding properties do not contain a bean name, a default - * {@link MessagingMessageConverter} is returned; if the binder properties contain a - * header mapper bean name, it is used in the default converter, otherwise a - * {@link DefaultKafkaHeaderMapper} is used. - * @param applicationContext the application context. - * @param extendedConsumerProperties the consumer binding properties. - * @param configurationProperties the binder properties. - * @return the converter - * @throws IllegalStateException if a bean name is specified but not found. - */ - public static MessageConverter getConsumerMessageConverter(ApplicationContext applicationContext, - ExtendedConsumerProperties extendedConsumerProperties, - KafkaBinderConfigurationProperties configurationProperties) { - - MessageConverter messageConverter; - if (extendedConsumerProperties.getExtension().getConverterBeanName() == null) { - MessagingMessageConverter mmc = new MessagingMessageConverter(); - StandardHeaders standardHeaders = extendedConsumerProperties.getExtension() - .getStandardHeaders(); - mmc.setGenerateMessageId(StandardHeaders.id.equals(standardHeaders) - || StandardHeaders.both.equals(standardHeaders)); - mmc.setGenerateTimestamp( - StandardHeaders.timestamp.equals(standardHeaders) - || StandardHeaders.both.equals(standardHeaders)); - KafkaHeaderMapper headerMapper = getHeaderMapper(applicationContext, configurationProperties); - if (headerMapper == null) { - headerMapper = new DefaultKafkaHeaderMapper(); - } - mmc.setHeaderMapper(headerMapper); - messageConverter = mmc; - } - else { - try { - messageConverter = applicationContext.getBean( - extendedConsumerProperties.getExtension().getConverterBeanName(), MessageConverter.class); - } - catch (NoSuchBeanDefinitionException ex) { - throw new IllegalStateException( - "Converter bean not present in application context", ex); - } - } - return messageConverter; - } - - /** - * Get the header mapper bean, if the binder properties contains a bean name; if not - * look for a bean with name {@code kafkaBinderHeaderMapper} is looked up; if that - * doesn't exist, null is returned. - * @param applicationContext the application context. - * @param configurationProperties the binder properties. - * @return the mapper. - */ - @Nullable - public static KafkaHeaderMapper getHeaderMapper(ApplicationContext applicationContext, - KafkaBinderConfigurationProperties configurationProperties) { - - KafkaHeaderMapper mapper = null; - if (configurationProperties.getHeaderMapperBeanName() != null) { - mapper = applicationContext.getBean( - configurationProperties.getHeaderMapperBeanName(), - KafkaHeaderMapper.class); - } - if (mapper == null) { - //First, try to see if there is a bean named headerMapper registered by other frameworks using the binder (for e.g. spring cloud sleuth) - try { - mapper = applicationContext.getBean("kafkaBinderHeaderMapper", KafkaHeaderMapper.class); - } - catch (BeansException be) { - } - } - return mapper; - } - - /** - * Create the Kafka configuration map for a consumer binding. With anonymous bindings - * (those without a {@code group} property, which are given a {@code UUID.toString()} - * in the group id) consumption begins from the current end of the topic, otherwise - * consumption starts from the beginning, the first time the binding consumes. - * @param anonymous true if this is for an anonymous binding. - * @param consumerGroup the group. - * @param consumerProperties the binding properties. - * @param configurationProperties the binder properties. - * @return the config map. - */ - public static Map createConsumerConfigs(boolean anonymous, String consumerGroup, - ExtendedConsumerProperties consumerProperties, - KafkaBinderConfigurationProperties configurationProperties) { - - Map props = new HashMap<>(); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); - props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 100); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, - anonymous ? "latest" : "earliest"); - props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroup); - - Map mergedConfig = configurationProperties.mergedConsumerConfiguration(); - if (!ObjectUtils.isEmpty(mergedConfig)) { - props.putAll(mergedConfig); - } - if (ObjectUtils.isEmpty(props.get(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG))) { - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, - configurationProperties.getKafkaConnectionString()); - } - Map config = consumerProperties.getExtension().getConfiguration(); - if (!ObjectUtils.isEmpty(config)) { - Assert.state(!config.containsKey(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG), - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG + " cannot be overridden at the binding level; " - + "use multiple binders instead"); - props.putAll(config); - } - if (!ObjectUtils.isEmpty(consumerProperties.getExtension().getStartOffset())) { - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, - consumerProperties.getExtension().getStartOffset().name()); - } - return props; - } - - /** - * Create the Kafka configuration map for a producer binding. - * @param producerProperties the binding properties. - * @param configurationProperties the binder properties. - * @return the config map. - */ - public static Map createProducerConfigs( - ExtendedProducerProperties producerProperties, - KafkaBinderConfigurationProperties configurationProperties) { - - Map props = new HashMap<>(); - props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); - props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, - ByteArraySerializer.class); - props.put(ProducerConfig.ACKS_CONFIG, - String.valueOf(configurationProperties.getRequiredAcks())); - Map mergedConfig = configurationProperties - .mergedProducerConfiguration(); - if (!ObjectUtils.isEmpty(mergedConfig)) { - props.putAll(mergedConfig); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG))) { - props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, - configurationProperties.getKafkaConnectionString()); - } - final KafkaProducerProperties kafkaProducerProperties = producerProperties.getExtension(); - if (ObjectUtils.isEmpty(props.get(ProducerConfig.BATCH_SIZE_CONFIG))) { - props.put(ProducerConfig.BATCH_SIZE_CONFIG, - String.valueOf(kafkaProducerProperties.getBufferSize())); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.LINGER_MS_CONFIG))) { - props.put(ProducerConfig.LINGER_MS_CONFIG, - String.valueOf(kafkaProducerProperties.getBatchTimeout())); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.COMPRESSION_TYPE_CONFIG))) { - props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, - kafkaProducerProperties.getCompressionType().toString()); - } - Map configs = producerProperties.getExtension().getConfiguration(); - Assert.state(!configs.containsKey(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG), - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG + " cannot be overridden at the binding level; " - + "use multiple binders instead"); - if (!ObjectUtils.isEmpty(configs)) { - props.putAll(configs); - } - if (!ObjectUtils.isEmpty(kafkaProducerProperties.getConfiguration())) { - props.putAll(kafkaProducerProperties.getConfiguration()); - } - return props; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/DlqDestinationResolver.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/DlqDestinationResolver.java deleted file mode 100644 index cd6d18484..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/DlqDestinationResolver.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.utils; - -import java.util.function.BiFunction; - -import org.apache.kafka.clients.consumer.ConsumerRecord; - -/** - * A {@link BiFunction} extension for defining DLQ destination resolvers. - * - * The BiFunction takes the {@link ConsumerRecord} and the exception as inputs - * and returns a topic name as the DLQ. - * - * @author Soby Chacko - * @since 3.0.9 - */ -@FunctionalInterface -public interface DlqDestinationResolver extends BiFunction, Exception, String> { - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/DlqPartitionFunction.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/DlqPartitionFunction.java deleted file mode 100644 index 8ed0534e2..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/DlqPartitionFunction.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.utils; - -import org.apache.commons.logging.Log; -import org.apache.kafka.clients.consumer.ConsumerRecord; - -import org.springframework.lang.Nullable; - -/** - * A TriFunction that takes a consumer group, consumer record, and throwable and returns - * which partition to publish to the dead letter topic. Returning {@code null} means Kafka - * will choose the partition. - * - * @author Gary Russell - * @since 3.0 - * - */ -@FunctionalInterface -public interface DlqPartitionFunction { - - /** - * Returns the same partition as the original recor. - */ - DlqPartitionFunction ORIGINAL_PARTITION = (group, rec, ex) -> rec.partition(); - - /** - * Returns 0. - */ - DlqPartitionFunction PARTITION_ZERO = (group, rec, ex) -> 0; - - /** - * Apply the function. - * @param group the consumer group. - * @param record the consumer record. - * @param throwable the exception. - * @return the DLQ partition, or null. - */ - @Nullable - Integer apply(String group, ConsumerRecord record, Throwable throwable); - - /** - * Determine the fallback function to use based on the dlq partition count if no - * {@link DlqPartitionFunction} bean is provided. - * @param dlqPartitions the partition count. - * @param logger the logger. - * @return the fallback. - */ - static DlqPartitionFunction determineFallbackFunction(@Nullable Integer dlqPartitions, Log logger) { - if (dlqPartitions == null) { - return ORIGINAL_PARTITION; - } - else if (dlqPartitions > 1) { - logger.error("'dlqPartitions' is > 1 but a custom DlqPartitionFunction bean is not provided"); - return ORIGINAL_PARTITION; - } - else { - return PARTITION_ZERO; - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/KafkaTopicUtils.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/KafkaTopicUtils.java deleted file mode 100644 index c3ee1bde2..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/main/java/org/springframework/cloud/stream/binder/kafka/utils/KafkaTopicUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka.utils; - -import java.nio.charset.StandardCharsets; - -/** - * Utility methods releated to Kafka topics. - * - * @author Soby Chacko - */ -public final class KafkaTopicUtils { - - private KafkaTopicUtils() { - - } - - /** - * Validate topic name. Allowed chars are ASCII alphanumerics, '.', '_' and '-'. - * @param topicName name of the topic - */ - public static void validateTopicName(String topicName) { - byte[] utf8 = topicName.getBytes(StandardCharsets.UTF_8); - for (byte b : utf8) { - if (!((b >= 'a') && (b <= 'z') || (b >= 'A') && (b <= 'Z') - || (b >= '0') && (b <= '9') || (b == '.') || (b == '-') - || (b == '_'))) { - throw new IllegalArgumentException( - "Topic name can only have ASCII alphanumerics, '.', '_' and '-', but was: '" - + topicName + "'"); - } - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBinderConfigurationPropertiesTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBinderConfigurationPropertiesTest.java deleted file mode 100644 index 4a36b03be..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/java/org/springframework/cloud/stream/binder/kafka/properties/KafkaBinderConfigurationPropertiesTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.properties; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.Map; - -import com.sun.net.httpserver.HttpServer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.assertj.core.util.Files; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.core.io.ClassPathResource; - -import static org.assertj.core.api.Assertions.assertThat; - -public class KafkaBinderConfigurationPropertiesTest { - - @Test - public void mergedConsumerConfigurationFiltersGroupIdFromKafkaProperties() { - KafkaProperties kafkaProperties = new KafkaProperties(); - kafkaProperties.getConsumer().setGroupId("group1"); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - - Map mergedConsumerConfiguration = - kafkaBinderConfigurationProperties.mergedConsumerConfiguration(); - - assertThat(mergedConsumerConfiguration).doesNotContainKeys(ConsumerConfig.GROUP_ID_CONFIG); - } - - @Test - public void mergedConsumerConfigurationFiltersEnableAutoCommitFromKafkaProperties() { - KafkaProperties kafkaProperties = new KafkaProperties(); - kafkaProperties.getConsumer().setEnableAutoCommit(true); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - - Map mergedConsumerConfiguration = - kafkaBinderConfigurationProperties.mergedConsumerConfiguration(); - - assertThat(mergedConsumerConfiguration).doesNotContainKeys(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG); - } - - @Test - public void mergedConsumerConfigurationFiltersGroupIdFromKafkaBinderConfigurationPropertiesConfiguration() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - kafkaBinderConfigurationProperties - .setConfiguration(Collections.singletonMap(ConsumerConfig.GROUP_ID_CONFIG, "group1")); - - Map mergedConsumerConfiguration = kafkaBinderConfigurationProperties.mergedConsumerConfiguration(); - - assertThat(mergedConsumerConfiguration).doesNotContainKeys(ConsumerConfig.GROUP_ID_CONFIG); - } - - @Test - public void mergedConsumerConfigurationFiltersEnableAutoCommitFromKafkaBinderConfigurationPropertiesConfiguration() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - kafkaBinderConfigurationProperties - .setConfiguration(Collections.singletonMap(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")); - - Map mergedConsumerConfiguration = kafkaBinderConfigurationProperties.mergedConsumerConfiguration(); - - assertThat(mergedConsumerConfiguration).doesNotContainKeys(ConsumerConfig.GROUP_ID_CONFIG); - } - - @Test - public void mergedConsumerConfigurationFiltersGroupIdFromKafkaBinderConfigurationPropertiesConsumerProperties() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - kafkaBinderConfigurationProperties - .setConsumerProperties(Collections.singletonMap(ConsumerConfig.GROUP_ID_CONFIG, "group1")); - - Map mergedConsumerConfiguration = kafkaBinderConfigurationProperties.mergedConsumerConfiguration(); - - assertThat(mergedConsumerConfiguration).doesNotContainKeys(ConsumerConfig.GROUP_ID_CONFIG); - } - - @Test - public void mergedConsumerConfigurationFiltersEnableAutoCommitFromKafkaBinderConfigurationPropertiesConsumerProps() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - kafkaBinderConfigurationProperties - .setConsumerProperties(Collections.singletonMap(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")); - - Map mergedConsumerConfiguration = kafkaBinderConfigurationProperties.mergedConsumerConfiguration(); - - assertThat(mergedConsumerConfiguration).doesNotContainKeys(ConsumerConfig.GROUP_ID_CONFIG); - } - - @Test - public void testCertificateFilesAreConvertedToAbsolutePathsFromClassPathResources() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - final Map configuration = kafkaBinderConfigurationProperties.getConfiguration(); - configuration.put("ssl.truststore.location", "classpath:testclient.truststore"); - configuration.put("ssl.keystore.location", "classpath:testclient.keystore"); - - kafkaBinderConfigurationProperties.getKafkaConnectionString(); - - assertThat(configuration.get("ssl.truststore.location")) - .isEqualTo(Paths.get(System.getProperty("java.io.tmpdir"), "testclient.truststore").toString()); - assertThat(configuration.get("ssl.keystore.location")) - .isEqualTo(Paths.get(System.getProperty("java.io.tmpdir"), "testclient.keystore").toString()); - deleteTempCertFiles(); - } - - @Test - public void testCertificateFilesAreConvertedToAbsolutePathsFromHttpResources() throws IOException { - HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 5869), 0); - createContextWithCertFileHandler(server, "testclient.truststore"); - createContextWithCertFileHandler(server, "testclient.keystore"); - server.setExecutor(null); // creates a default executor - server.start(); - - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - final Map configuration = kafkaBinderConfigurationProperties.getConfiguration(); - configuration.put("ssl.truststore.location", "http://localhost:5869/testclient.truststore"); - configuration.put("ssl.keystore.location", "http://localhost:5869/testclient.keystore"); - configuration.put("schema.registry.ssl.truststore.location", "http://localhost:5869/testclient.truststore"); - configuration.put("schema.registry.ssl.keystore.location", "http://localhost:5869/testclient.keystore"); - - kafkaBinderConfigurationProperties.getKafkaConnectionString(); - - assertThat(configuration.get("ssl.truststore.location")) - .isEqualTo(Paths.get(System.getProperty("java.io.tmpdir"), "testclient.truststore").toString()); - assertThat(configuration.get("ssl.keystore.location")) - .isEqualTo(Paths.get(System.getProperty("java.io.tmpdir"), "testclient.keystore").toString()); - assertThat(configuration.get("schema.registry.ssl.truststore.location")) - .isEqualTo(Paths.get(System.getProperty("java.io.tmpdir"), "testclient.truststore").toString()); - assertThat(configuration.get("schema.registry.ssl.keystore.location")) - .isEqualTo(Paths.get(System.getProperty("java.io.tmpdir"), "testclient.keystore").toString()); - deleteTempCertFiles(); - - server.stop(0); - } - - @Test - public void testCertificateFilesAreConvertedToGivenAbsolutePathsFromClassPathResources() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - final Map configuration = kafkaBinderConfigurationProperties.getConfiguration(); - configuration.put("ssl.truststore.location", "classpath:testclient.truststore"); - configuration.put("ssl.keystore.location", "classpath:testclient.keystore"); - kafkaBinderConfigurationProperties.setCertificateStoreDirectory("target"); - - kafkaBinderConfigurationProperties.getKafkaConnectionString(); - - assertThat(configuration.get("ssl.truststore.location")).isEqualTo( - Paths.get(Files.currentFolder().toString(), "target", "testclient.truststore").toString()); - assertThat(configuration.get("ssl.keystore.location")).isEqualTo( - Paths.get(Files.currentFolder().toString(), "target", "testclient.keystore").toString()); - } - - @Test - public void testCertificateFilesAreMovedForSchemaRegistryConfiguration() { - KafkaProperties kafkaProperties = new KafkaProperties(); - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = - new KafkaBinderConfigurationProperties(kafkaProperties); - final Map configuration = kafkaBinderConfigurationProperties.getConfiguration(); - configuration.put("schema.registry.ssl.truststore.location", "classpath:testclient.truststore"); - configuration.put("schema.registry.ssl.keystore.location", "classpath:testclient.keystore"); - kafkaBinderConfigurationProperties.setCertificateStoreDirectory("target"); - - kafkaBinderConfigurationProperties.getKafkaConnectionString(); - - assertThat(configuration.get("schema.registry.ssl.truststore.location")).isEqualTo( - Paths.get(Files.currentFolder().toString(), "target", "testclient.truststore").toString()); - assertThat(configuration.get("schema.registry.ssl.keystore.location")).isEqualTo( - Paths.get(Files.currentFolder().toString(), "target", "testclient.keystore").toString()); - } - - private void createContextWithCertFileHandler(HttpServer server, String path) { - server.createContext("/" + path, exchange -> { - ClassPathResource ts = new ClassPathResource(path); - byte[] response = ts.getContentAsByteArray(); - exchange.sendResponseHeaders(200, response.length); - OutputStream os = exchange.getResponseBody(); - os.write(response); - os.close(); - }); - } - - private void deleteTempCertFiles() { - Paths.get(System.getProperty("java.io.tmpdir"), "testclient.truststore").toFile().delete(); - Paths.get(System.getProperty("java.io.tmpdir"), "testclient.keystore").toFile().delete(); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/java/org/springframework/cloud/stream/binder/kafka/provisioning/KafkaTopicProvisionerTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/java/org/springframework/cloud/stream/binder/kafka/provisioning/KafkaTopicProvisionerTests.java deleted file mode 100644 index de2c8c17a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/java/org/springframework/cloud/stream/binder/kafka/provisioning/KafkaTopicProvisionerTests.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.provisioning; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.apache.kafka.clients.CommonClientConfigs; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.common.config.SslConfigs; -import org.apache.kafka.common.network.SslChannelBuilder; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.core.io.ClassPathResource; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -/** - * @author Gary Russell - * @since 2.0 - * - */ -public class KafkaTopicProvisionerTests { - - AdminClientConfigCustomizer adminClientConfigCustomizer = adminClientProperties -> adminClientProperties.put("foo", "bar"); - - @SuppressWarnings("rawtypes") - @Test - public void bootPropertiesOverriddenExceptServers() throws Exception { - KafkaProperties bootConfig = new KafkaProperties(); - bootConfig.getProperties().put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, - "PLAINTEXT"); - bootConfig.setBootstrapServers(Collections.singletonList("localhost:1234")); - KafkaBinderConfigurationProperties binderConfig = new KafkaBinderConfigurationProperties( - bootConfig); - binderConfig.getConfiguration().put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, - "SSL"); - ClassPathResource ts = new ClassPathResource("test.truststore.ks"); - binderConfig.getConfiguration().put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, - ts.getFile().getAbsolutePath()); - binderConfig.setBrokers("localhost:9092"); - KafkaTopicProvisioner provisioner = new KafkaTopicProvisioner(binderConfig, - bootConfig, adminClientConfigCustomizer); - AdminClient adminClient = provisioner.createAdminClient(); - assertThat(KafkaTestUtils.getPropertyValue(adminClient, - "client.selector.channelBuilder")).isInstanceOf(SslChannelBuilder.class); - Map configs = KafkaTestUtils.getPropertyValue(adminClient, - "client.selector.channelBuilder.configs", Map.class); - assertThat( - ((List) configs.get(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG)).get(0)) - .isEqualTo("localhost:1234"); - assertThat(configs.get("foo")).isEqualTo("bar"); - adminClient.close(); - } - - @SuppressWarnings("rawtypes") - @Test - public void bootPropertiesOverriddenIncludingServers() throws Exception { - KafkaProperties bootConfig = new KafkaProperties(); - bootConfig.getProperties().put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, - "PLAINTEXT"); - bootConfig.setBootstrapServers(Collections.singletonList("localhost:9092")); - KafkaBinderConfigurationProperties binderConfig = new KafkaBinderConfigurationProperties( - bootConfig); - binderConfig.getConfiguration().put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, - "SSL"); - ClassPathResource ts = new ClassPathResource("test.truststore.ks"); - binderConfig.getConfiguration().put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, - ts.getFile().getAbsolutePath()); - binderConfig.setBrokers("localhost:1234"); - KafkaTopicProvisioner provisioner = new KafkaTopicProvisioner(binderConfig, - bootConfig, adminClientConfigCustomizer); - AdminClient adminClient = provisioner.createAdminClient(); - assertThat(KafkaTestUtils.getPropertyValue(adminClient, - "client.selector.channelBuilder")).isInstanceOf(SslChannelBuilder.class); - Map configs = KafkaTestUtils.getPropertyValue(adminClient, - "client.selector.channelBuilder.configs", Map.class); - assertThat( - ((List) configs.get(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG)).get(0)) - .isEqualTo("localhost:1234"); - adminClient.close(); - } - - @Test - public void brokersInvalid() throws Exception { - KafkaProperties bootConfig = new KafkaProperties(); - KafkaBinderConfigurationProperties binderConfig = new KafkaBinderConfigurationProperties( - bootConfig); - binderConfig.getConfiguration().put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, - "localhost:1234"); - try { - new KafkaTopicProvisioner(binderConfig, bootConfig, adminClientConfigCustomizer); - fail("Expected illegal state"); - } - catch (IllegalStateException e) { - assertThat(e.getMessage()).isEqualTo( - "Set binder bootstrap servers via the 'brokers' property, not 'configuration'"); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/test.truststore.ks b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/test.truststore.ks deleted file mode 100644 index 24ead4bc6..000000000 Binary files a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/test.truststore.ks and /dev/null differ diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/testclient.keystore b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/testclient.keystore deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/testclient.truststore b/binders/kafka-binder/spring-cloud-stream-binder-kafka-core/src/test/resources/testclient.truststore deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/pom.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/pom.xml deleted file mode 100644 index 006bde647..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/pom.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-binder-kafka-reactive - jar - spring-cloud-stream-binder-kafka-reactive - Kafka binder implementation - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-parent - 4.1.0-SNAPSHOT - - - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-core - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.boot - spring-boot-autoconfigure - true - - - org.apache.kafka - kafka-clients - - - org.springframework.kafka - spring-kafka - - - io.projectreactor.kafka - reactor-kafka - - - org.springframework.boot - spring-boot-test - test - - - org.springframework.kafka - spring-kafka-test - test - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.apache.kafka - kafka-clients - test - - - org.apache.kafka - kafka_2.13 - - - org.apache.kafka - kafka_2.13 - test - - - org.awaitility - awaitility - test - - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinder.java deleted file mode 100644 index 91ec7fc41..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinder.java +++ /dev/null @@ -1,473 +0,0 @@ -/* - * 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.cloud.stream.binder.reactorkafka; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.Producer; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.PartitionInfo; -import reactor.core.publisher.Flux; -import reactor.kafka.receiver.KafkaReceiver; -import reactor.kafka.receiver.ReceiverOptions; -import reactor.kafka.receiver.ReceiverRecord; -import reactor.kafka.sender.KafkaSender; -import reactor.kafka.sender.SenderOptions; -import reactor.kafka.sender.SenderRecord; -import reactor.kafka.sender.SenderResult; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.support.ConsumerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.utils.BindingUtils; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.context.Lifecycle; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.endpoint.MessageProducerSupport; -import org.springframework.integration.handler.AbstractMessageHandler; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.support.KafkaHeaders; -import org.springframework.kafka.support.converter.KafkaMessageHeaders; -import org.springframework.kafka.support.converter.MessageConverter; -import org.springframework.kafka.support.converter.MessagingMessageConverter; -import org.springframework.kafka.support.converter.RecordMessageConverter; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * @author Gary Russell - * @author Byungjun You - * @since 4.0 - * - */ -public class ReactorKafkaBinder - extends AbstractMessageChannelBinder, - ExtendedProducerProperties, KafkaTopicProvisioner> - implements - ExtendedPropertiesBinder { - - private static final Log logger = LogFactory.getLog(ReactorKafkaBinder.class); - - private final KafkaBinderConfigurationProperties configurationProperties; - - private KafkaExtendedBindingProperties extendedBindingProperties = new KafkaExtendedBindingProperties(); - - private ConsumerConfigCustomizer consumerConfigCustomizer; - - private ProducerConfigCustomizer producerConfigCustomizer; - - private ReceiverOptionsCustomizer receiverOptionsCustomizer = (name, opts) -> opts; - - private SenderOptionsCustomizer senderOptionsCustomizer = (name, opts) -> opts; - - private final Map topicsInUse = new ConcurrentHashMap<>(); - - private final Map messageProducers = new ConcurrentHashMap<>(); - - public ReactorKafkaBinder(KafkaBinderConfigurationProperties configurationProperties, - KafkaTopicProvisioner provisioner) { - - super(new String[0], provisioner, null, null); - this.configurationProperties = configurationProperties; - } - - public void setConsumerConfigCustomizer(ConsumerConfigCustomizer consumerConfigCustomizer) { - this.consumerConfigCustomizer = consumerConfigCustomizer; - } - - public void setProducerConfigCustomizer(ProducerConfigCustomizer producerConfigCustomizer) { - this.producerConfigCustomizer = producerConfigCustomizer; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void receiverOptionsCustomizers(ObjectProvider customizers) { - if (customizers.getIfUnique() != null) { - this.receiverOptionsCustomizer = customizers.getIfUnique(); - } - else { - List list = customizers.orderedStream().toList(); - ReceiverOptionsCustomizer customizer = (name, opts) -> { - ReceiverOptions last = null; - for (ReceiverOptionsCustomizer cust: list) { - last = (ReceiverOptions) cust.apply(name, opts); - } - return last; - }; - if (!list.isEmpty()) { - this.receiverOptionsCustomizer = customizer; - } - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void senderOptionsCustomizers(ObjectProvider customizers) { - if (customizers.getIfUnique() != null) { - this.senderOptionsCustomizer = customizers.getIfUnique(); - } - else { - List list = customizers.orderedStream().toList(); - SenderOptionsCustomizer customizer = (name, opts) -> { - SenderOptions last = null; - for (SenderOptionsCustomizer cust: list) { - last = (SenderOptions) cust.apply(name, opts); - } - return last; - }; - if (!list.isEmpty()) { - this.senderOptionsCustomizer = customizer; - } - } - } - - @Override - protected MessageHandler createProducerMessageHandler(ProducerDestination destination, - ExtendedProducerProperties producerProperties, MessageChannel errorChannel) - throws Exception { - - Map configs = BindingUtils.createProducerConfigs(producerProperties, - this.configurationProperties); - if (this.producerConfigCustomizer != null) { - this.producerConfigCustomizer.configure(configs, producerProperties.getBindingName(), - destination.getName()); - } - Map props = BindingUtils.createProducerConfigs(producerProperties, - this.configurationProperties); - DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>(props); - Collection partitions = provisioningProvider.getPartitionsForTopic( - producerProperties.getPartitionCount(), false, () -> { - Producer producer = producerFactory.createProducer(); - List partitionsFor = producer - .partitionsFor(destination.getName()); - producer.close(); - return partitionsFor; - }, destination.getName()); - - this.topicsInUse.put(destination.getName(), - new TopicInformation(null, partitions, false)); - - SenderOptions opts = this.senderOptionsCustomizer.apply(producerProperties.getBindingName(), - SenderOptions.create(configs)); - // TODO bean for converter; MCB doesn't use one on the producer side. - RecordMessageConverter converter = new MessagingMessageConverter(); - AbstractApplicationContext applicationContext = getApplicationContext(); - FluxMessageChannel resultChannel = null; - String channelName = producerProperties.getExtension().getRecordMetadataChannel(); - if (channelName != null && applicationContext.containsBean(channelName)) { - resultChannel = applicationContext.getBean(channelName, FluxMessageChannel.class); - } - return new ReactorMessageHandler(opts, converter, destination.getName(), resultChannel); - } - - // TODO: Refactor to provide in a common area since KafkaMessageChannelBinder also provides this. - public void processTopic(final String group, final ExtendedConsumerProperties extendedConsumerProperties, - final ConsumerFactory consumerFactory, int partitionCount, - boolean usingPatterns, boolean groupManagement, String topic) { - Collection listenedPartitions; - Collection allPartitions = usingPatterns ? Collections.emptyList() - : getPartitionInfo(topic, extendedConsumerProperties, consumerFactory, - partitionCount); - - if (groupManagement || extendedConsumerProperties.getInstanceCount() == 1) { - listenedPartitions = allPartitions; - } - else { - listenedPartitions = new ArrayList<>(); - for (PartitionInfo partition : allPartitions) { - // divide partitions across modules - if ((partition.partition() % extendedConsumerProperties - .getInstanceCount()) == extendedConsumerProperties - .getInstanceIndex()) { - listenedPartitions.add(partition); - } - } - } - this.topicsInUse.put(topic, - new TopicInformation(group, listenedPartitions, usingPatterns)); - } - - private Collection getPartitionInfo(String topic, - final ExtendedConsumerProperties extendedConsumerProperties, - final ConsumerFactory consumerFactory, int partitionCount) { - return provisioningProvider.getPartitionsForTopic(partitionCount, - extendedConsumerProperties.getExtension().isAutoRebalanceEnabled(), - () -> { - try (Consumer consumer = consumerFactory.createConsumer()) { - return consumer.partitionsFor(topic); - } - }, topic); - } - - Map getTopicsInUse() { - return this.topicsInUse; - } - - - @Override - protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group, - ExtendedConsumerProperties properties) { - - boolean anonymous = !StringUtils.hasText(group); - String consumerGroup = anonymous ? "anonymous." + UUID.randomUUID() : group; - Map configs = BindingUtils.createConsumerConfigs(anonymous, consumerGroup, properties, - this.configurationProperties); - - String destinations = destination.getName(); - if (this.consumerConfigCustomizer != null) { - this.consumerConfigCustomizer.configure(configs, properties.getBindingName(), destinations); - } - - MessageConverter converter = BindingUtils.getConsumerMessageConverter(getApplicationContext(), properties, - this.configurationProperties); - Assert.isInstanceOf(RecordMessageConverter.class, converter); - /* - * No need to check multiplex here because, if false, the topics are bound one-at-a-time; - * it is still required by the provisioner, however. - */ - List destList = Arrays.stream(StringUtils.commaDelimitedListToStringArray(destinations)) - .map(String::trim) - .toList(); - ReceiverOptions opts = ReceiverOptions.create(configs) - .addAssignListener(parts -> logger.info("Assigned: " + parts)); - if (properties.getExtension().isDestinationIsPattern()) { - opts = opts.subscription(Pattern.compile(destinations)); - } - else { - opts = opts.subscription(destList); - } - opts = this.receiverOptionsCustomizer.apply(properties.getBindingName(), opts); - ReceiverOptions finalOpts = opts; - - Map props = BindingUtils.createConsumerConfigs(anonymous, consumerGroup, properties, - this.configurationProperties); - - DefaultKafkaConsumerFactory factory = new DefaultKafkaConsumerFactory<>(props); - int partitionCount = properties.getInstanceCount() * properties.getConcurrency(); - boolean groupManagement = properties.getExtension().isAutoRebalanceEnabled(); - processTopic(consumerGroup, properties, factory, partitionCount, properties.getExtension().isDestinationIsPattern(), - groupManagement, destination.getName()); - - class ReactorMessageProducer extends MessageProducerSupport { - - private final List> receivers = new ArrayList<>(); - - ReactorMessageProducer() { - for (int i = 0; i < properties.getConcurrency(); i++) { - this.receivers.add(KafkaReceiver.create(finalOpts)); - } - } - - @SuppressWarnings("unchecked") - @Override - protected void doStart() { - List>> fluxes = new ArrayList<>(); - int concurrency = properties.getConcurrency(); - boolean autoCommit = properties.getExtension().isReactiveAutoCommit(); - boolean atMostOnce = properties.getExtension().isReactiveAtMostOnce(); - Assert.state(!(autoCommit && atMostOnce), - "Cannot set both reactiveAutoCommit and reactiveAtMostOnce"); - for (int i = 0; i < concurrency; i++) { - Flux> receive = null; - KafkaReceiver kafkaReceiver = this.receivers.get(i); - if (atMostOnce) { - receive = kafkaReceiver - .receiveAtmostOnce(); - } - else if (!autoCommit) { - receive = kafkaReceiver - .receive(); - } - if (autoCommit) { - fluxes.add(kafkaReceiver - .receiveAutoAck() - .map(inner -> new GenericMessage<>(inner))); - } - else { - fluxes.add(receive - .map(record -> { - Message message = (Message) ((RecordMessageConverter) converter) - .toMessage(record, null, null, null); - return addAckHeaderIfNeeded(atMostOnce, record, message); - })); - } - } - if (concurrency == 1) { - subscribeToPublisher(fluxes.get(0)); - } - else { - subscribeToPublisher(Flux.merge(fluxes)); - } - } - - private Message addAckHeaderIfNeeded(boolean autoCommit, ConsumerRecord record, - Message message) { - - if (!autoCommit) { - if (message.getHeaders() instanceof KafkaMessageHeaders headers) { - headers.getRawHeaders().put(KafkaHeaders.ACKNOWLEDGMENT, - ((ReceiverRecord) record).receiverOffset()); - } - else { - message = MessageBuilder.fromMessage(message) - .setHeader(KafkaHeaders.ACKNOWLEDGMENT, - ((ReceiverRecord) record).receiverOffset()) - .build(); - } - } - return message; - } - - } - ReactorMessageProducer reactorMessageProducer = new ReactorMessageProducer(); - this.messageProducers.put(consumerGroup, reactorMessageProducer); - return reactorMessageProducer; - } - - public Map getMessageProducers() { - return this.messageProducers; - } - - @Override - public KafkaConsumerProperties getExtendedConsumerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedConsumerProperties(channelName); - } - - @Override - public KafkaProducerProperties getExtendedProducerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedProducerProperties(channelName); - } - - @Override - public String getDefaultsPrefix() { - return this.extendedBindingProperties.getDefaultsPrefix(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return this.extendedBindingProperties.getExtendedPropertiesEntryClass(); - } - - public void setExtendedBindingProperties( - KafkaExtendedBindingProperties extendedBindingProperties) { - - this.extendedBindingProperties = extendedBindingProperties; - } - - private static class ReactorMessageHandler extends AbstractMessageHandler implements Lifecycle { - - private final RecordMessageConverter converter; - - private final String topic; - - private final SenderOptions senderOptions; - - @Nullable - private final FluxMessageChannel results; - - private volatile KafkaSender sender; - - private volatile boolean running; - - ReactorMessageHandler(SenderOptions opts, RecordMessageConverter converter, - String topic, @Nullable FluxMessageChannel results) { - - this.senderOptions = opts; - this.converter = converter; - this.topic = topic; - this.results = results; - } - - @Override - protected void handleMessageInternal(Message message) { - if (this.sender != null) { - Object correlation = message.getHeaders().get(IntegrationMessageHeaderAccessor.CORRELATION_ID); - if (correlation == null) { - correlation = UUID.randomUUID(); - } - @SuppressWarnings("unchecked") - SenderRecord sr = SenderRecord.create( - (ProducerRecord) converter.fromMessage(message, topic), correlation); - Flux> result = sender.send(Flux.just(sr)); - result.subscribe(res -> { - if (this.results != null) { - this.results.send(MessageBuilder.withPayload(res) - .copyHeaders(message.getHeaders()) - .build()); - } - }); - } - } - - @Override - public synchronized void start() { - if (!this.running) { - this.sender = KafkaSender.create(this.senderOptions); - this.running = true; - } - } - - @Override - public synchronized void stop() { - if (this.running) { - KafkaSender theSender = this.sender; - this.sender = null; - theSender.close(); - this.running = false; - } - } - - @Override - public boolean isRunning() { - return this.running; - } - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderConfiguration.java deleted file mode 100644 index 5a740f897..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderConfiguration.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.reactorkafka; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.support.ConsumerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Binder configuration for ReactorKafka. - * - * @author Gary Russell - * @author Chris Bono - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(Binder.class) -@EnableConfigurationProperties({ KafkaProperties.class, KafkaExtendedBindingProperties.class }) -public class ReactorKafkaBinderConfiguration { - - /** - * @ConfigurationProperties is declared on the @Bean method for Spring Boot to ignore - * constructor binding on KafkaBinderConfigurationProperties. If constructor binding is - * used, it ignores all the JavaBeans style properties when generating configuration metadata. - * - * See the following issues for more details: - * - * https://github.com/spring-cloud/spring-cloud-stream/issues/2640 - * https://github.com/spring-projects/spring-boot/issues/34031 - * - * @param kafkaProperties Spring Kafka properties autoconfigured by Spring Boot - */ - @Bean - @ConfigurationProperties(prefix = "spring.cloud.stream.kafka.binder") - KafkaBinderConfigurationProperties configurationProperties( - KafkaProperties kafkaProperties) { - return new KafkaBinderConfigurationProperties(kafkaProperties); - } - - @Bean - KafkaTopicProvisioner provisioningProvider( - KafkaBinderConfigurationProperties configurationProperties, - ObjectProvider adminClientConfigCustomizer, KafkaProperties kafkaProperties) { - return new KafkaTopicProvisioner(configurationProperties, - kafkaProperties, adminClientConfigCustomizer.getIfUnique()); - } - - @Bean - ReactorKafkaBinder reactorKafkaBinder(KafkaBinderConfigurationProperties configurationProperties, - KafkaTopicProvisioner provisioningProvider, - KafkaExtendedBindingProperties extendedBindingProperties, - ObjectProvider consumerConfigCustomizer, - ObjectProvider producerConfigCustomizer, - ObjectProvider receiverOptionsCustomizers, - ObjectProvider senderOptionsptionsCustomizers) { - - ReactorKafkaBinder reactorKafkaBinder = new ReactorKafkaBinder(configurationProperties, provisioningProvider); - reactorKafkaBinder.setExtendedBindingProperties(extendedBindingProperties); - reactorKafkaBinder.setConsumerConfigCustomizer(consumerConfigCustomizer.getIfUnique()); - reactorKafkaBinder.setProducerConfigCustomizer(producerConfigCustomizer.getIfUnique()); - reactorKafkaBinder.receiverOptionsCustomizers(receiverOptionsCustomizers); - reactorKafkaBinder.senderOptionsCustomizers(senderOptionsptionsCustomizers); - return reactorKafkaBinder; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderHealthIndicator.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderHealthIndicator.java deleted file mode 100644 index 3f4887df8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderHealthIndicator.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.reactorkafka; - -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.Executors; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.cloud.stream.binder.kafka.common.AbstractKafkaBinderHealthIndicator; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.integration.endpoint.MessageProducerSupport; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.scheduling.concurrent.CustomizableThreadFactory; - -/** - * {@link org.springframework.boot.actuate.health.HealthIndicator} for Reactor Kafka Binder. - * - * @author Soby Chacko - */ -public class ReactorKafkaBinderHealthIndicator extends AbstractKafkaBinderHealthIndicator { - - private final ReactorKafkaBinder binder; - - public ReactorKafkaBinderHealthIndicator(ReactorKafkaBinder binder, ConsumerFactory consumerFactory) { - super(consumerFactory); - this.binder = binder; - } - - @Override - protected ExecutorService createHealthBinderExecutorService() { - return Executors.newSingleThreadExecutor( - new CustomizableThreadFactory("reactor-kafka-binder-health-")); - } - - @Override - protected Map getTopicsInUse() { - return this.binder.getTopicsInUse(); - } - - @Override - protected Health buildBinderSpecificHealthDetails() { - Map messageProducerSupportInfo = binder.getMessageProducers(); - if (messageProducerSupportInfo.isEmpty()) { - return Health.unknown().build(); - } - - Status status = Status.UP; - List> messageProducers = new ArrayList<>(); - - Map messageProducerDetails = new HashMap<>(); - for (String groupId : messageProducerSupportInfo.keySet()) { - MessageProducerSupport messageProducerSupport = messageProducerSupportInfo.get(groupId); - boolean isRunning = messageProducerSupport.isRunning(); - boolean isOk = messageProducerSupport.isActive(); - if (!isOk) { - status = Status.DOWN; - } - messageProducerDetails.put("isRunning", isRunning); - messageProducerDetails.put("isStoppedAbnormally", !isRunning && !isOk); - //messageProducerDetails.put("isPaused", messageProducerSupport.isPaused()); - messageProducerDetails.put("messageProducerId", messageProducerSupport.getApplicationContextId()); - messageProducerDetails.put("groupId", groupId); - } - messageProducers.add(messageProducerDetails); - return Health.status(status) - .withDetail("messageProducers", messageProducers) - .build(); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReceiverOptionsCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReceiverOptionsCustomizer.java deleted file mode 100644 index 2f2fbc230..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/ReceiverOptionsCustomizer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.reactorkafka; - -import java.util.function.BiFunction; - -import reactor.kafka.receiver.ReceiverOptions; - -import org.springframework.core.Ordered; - -/** - * Customizer for {@link ReceiverOptions}; the first parameter contains the binder name; - * the second is the {@link ReceiverOptions} to customize. Return the customized - * {@link ReceiverOptions}. Applied in {@link Ordered order} if multiple customizers are - * found. - *

- * Generic since 4.0.3 to allow customization of deserializers. - * - * @param the key type. - * @param the value type. - * - * @author Gary Russell - * @since 4.0.2 - * - */ -public interface ReceiverOptionsCustomizer - extends BiFunction, ReceiverOptions>, Ordered { - - @Override - default int getOrder() { - return 0; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/SenderOptionsCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/SenderOptionsCustomizer.java deleted file mode 100644 index 37070e865..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/SenderOptionsCustomizer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.reactorkafka; - -import java.util.function.BiFunction; - -import reactor.kafka.sender.SenderOptions; - -import org.springframework.core.Ordered; - -/** - * Customizer for {@link SenderOptions}; the first parameter contains the binder name; the - * second is the {@link SenderOptions} to customize. Return the customized - * {@link SenderOptions}. Applied in {@link Ordered order} if multiple customizers are - * found. - *

- * Generic since 4.0.3 to allow customization of serializers. - * - * @param the key type. - * @param the value type. - * - * @author Gary Russell - * @since 4.0.2 - * - */ -public interface SenderOptionsCustomizer - extends BiFunction, SenderOptions>, Ordered { - - @Override - default int getOrder() { - return 0; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/package-info.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/package-info.java deleted file mode 100644 index a12de78b6..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/java/org/springframework/cloud/stream/binder/reactorkafka/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2021-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. - */ - -/** - * Provides classes reactor-kafka binder support. - */ -package org.springframework.cloud.stream.binder.reactorkafka; diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/resources/META-INF/spring.binders b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/resources/META-INF/spring.binders deleted file mode 100644 index ee8843881..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -reactorKafka:\ -org.springframework.cloud.stream.binder.reactorkafka.ReactorKafkaBinderConfiguration diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/resources/META-INF/spring.factories b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/resources/META-INF/spring.factories deleted file mode 100644 index dd6e28b8a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor:\ -org.springframework.cloud.stream.binder.kafka.common.KafkaBinderEnvironmentPostProcessor diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderHealthIndicatorTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderHealthIndicatorTests.java deleted file mode 100644 index c8b39414c..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderHealthIndicatorTests.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.reactorkafka; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.common.Node; -import org.apache.kafka.common.PartitionInfo; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.integration.endpoint.MessageProducerSupport; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; - -import static java.util.Collections.singleton; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class ReactorKafkaBinderHealthIndicatorTests { - - private static final String TEST_TOPIC = "test"; - - private ReactorKafkaBinderHealthIndicator indicator; - - @Mock - private DefaultKafkaConsumerFactory consumerFactory; - - @Mock - private KafkaConsumer consumer; - - @Mock - MessageProducerSupport messageProducerSupport1; - - @Mock - private ReactorKafkaBinder binder; - - private final Map topicsInUse = new HashMap<>(); - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - org.mockito.BDDMockito.given(consumerFactory.createConsumer()) - .willReturn((consumer)); - org.mockito.BDDMockito.given(binder.getTopicsInUse()).willReturn(topicsInUse); - this.indicator = new ReactorKafkaBinderHealthIndicator(binder, consumerFactory); - this.indicator.setTimeout(10); - } - - @Test - void reactorKafkaBinderIsUp() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group1-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - org.mockito.BDDMockito.given(binder.getMessageProducers()) - .willReturn(Map.of("group1-healthIndicator", messageProducerSupport1)); - org.mockito.BDDMockito.given(messageProducerSupport1.isRunning()).willReturn(true); - org.mockito.BDDMockito.given(messageProducerSupport1.isActive()).willReturn(true); - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("topicsInUse", singleton(TEST_TOPIC)); - assertThat(health.getDetails()).hasEntrySatisfying("messageProducers", value -> - assertThat((ArrayList) value).hasSize(1)); - } - - private List partitions(Node leader) { - List partitions = new ArrayList<>(); - partitions.add(new PartitionInfo(TEST_TOPIC, 0, leader, null, null)); - return partitions; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderIntegrationTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderIntegrationTests.java deleted file mode 100644 index 79c7bb4b8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderIntegrationTests.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.reactorkafka; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import reactor.core.publisher.Flux; -import reactor.kafka.receiver.ReceiverOptions; -import reactor.kafka.receiver.ReceiverRecord; -import reactor.kafka.sender.SenderResult; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.annotation.ServiceActivator; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.support.Acknowledgment; -import org.springframework.kafka.support.converter.MessagingMessageConverter; -import org.springframework.kafka.support.converter.RecordMessageConverter; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.messaging.Message; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -/** - * Integration tests for {@link ReactorKafkaBinder}. - * - * @author Soby Chacko - * @author Gary Russell - * @author Chris Bono - */ -@ExtendWith(SpringExtension.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -@EmbeddedKafka(topics = { "uppercased-words", "lowercased-words" }) -class ReactorKafkaBinderIntegrationTests { - - private static final List recOptsCustOrder = Collections.synchronizedList(new ArrayList<>()); - - private static final List patternedDeliveries = Collections.synchronizedList(new ArrayList<>()); - - @Autowired - private EmbeddedKafkaBroker embeddedKafka; - - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void endToEndReactorKafkaBinder(boolean excludeKafkaAutoConfig) { - - recOptsCustOrder.clear(); - patternedDeliveries.clear(); - Map consumerProps = KafkaTestUtils.consumerProps("group1", "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - Consumer consumer1 = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer1, "uppercased-words"); - Consumer consumer2 = cf.createConsumer("group2", null); - embeddedKafka.consumeFromEmbeddedTopics(consumer2, "lowercased-words"); - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(ReactiveKafkaApplication.class) - .web(WebApplicationType.NONE).run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.kafka.consumer.metadata.max.age.ms=1000", - "--spring.cloud.function.definition=uppercase;lowercase;patternConsumer", - "--spring.cloud.stream.function.reactive.uppercase=true", - "--spring.cloud.stream.function.reactive.lowercase=true", - "--spring.cloud.stream.bindings.uppercase-in-0.group=grp1", - "--spring.cloud.stream.bindings.uppercase-in-0.destination=words1", - "--spring.cloud.stream.bindings.uppercase-out-0.destination=uppercased-words", - "--spring.cloud.stream.bindings.lowercase-in-0.group=grp2", - "--spring.cloud.stream.bindings.lowercase-in-0.destination=words2", - "--spring.cloud.stream.bindings.lowercase-out-0.destination=lowercased-words", - "--spring.cloud.stream.bindings.patternConsumer-in-0.group=grp3", - "--spring.cloud.stream.bindings.patternConsumer-in-0.destination=.*-words", - "--spring.cloud.stream.kafka.bindings.words1.producer.record-metadata-channel=sendResults", - "--spring.cloud.stream.kafka.bindings.words2.producer.record-metadata-channel=sendResults", - "--spring.cloud.stream.kafka.bindings.patternConsumer-in-0.consumer.destination-is-pattern=true", - "--spring.cloud.stream.kafka.bindings.lowercase-in-0.consumer.converterBeanName=fullRR", - "--spring.cloud.stream.kafka.binder.brokers=" + embeddedKafka.getBrokersAsString(), - excludeKafkaAutoConfigParam(excludeKafkaAutoConfig))) { - - StreamBridge streamBridge = context.getBean(StreamBridge.class); - streamBridge.send("words1", MessageBuilder.withPayload("foobar") - .setCorrelationId(42) - .build()); - streamBridge.send("words2", MessageBuilder.withPayload("BAZQUX") - .setCorrelationId(43) - .build()); - - assertThat(KafkaTestUtils.getSingleRecord(consumer1, "uppercased-words")) - .isNotNull() - .extracting(ConsumerRecord::value) - .isEqualTo("FOOBAR"); - - assertThat(KafkaTestUtils.getSingleRecord(consumer2, "lowercased-words")) - .isNotNull() - .extracting(ConsumerRecord::value) - .isEqualTo("bazqux"); - - assertThat(recOptsCustOrder).containsExactly("two", "one", "two", "one", "two", "one"); - await().untilAsserted(() -> assertThat(patternedDeliveries).contains("bazqux", "FOOBAR")); - assertThat(context.getBean(ReactiveKafkaApplication.class).correlation).contains(42, 43); - } - } - - private String excludeKafkaAutoConfigParam(boolean excludeKafkaAutoConfig) { - return excludeKafkaAutoConfig ? - "--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration" : "foo=bar"; - } - - @EnableAutoConfiguration - public static class ReactiveKafkaApplication { - - final List correlation = Collections.synchronizedList(new ArrayList<>()); - - @Bean - RecordMessageConverter fullRR() { - return new RecordMessageConverter() { - - private final RecordMessageConverter converter = new MessagingMessageConverter(); - - @Override - public Message toMessage(ConsumerRecord record, Acknowledgment acknowledgment, - Consumer consumer, Type payloadType) { - - return MessageBuilder.withPayload(record).build(); - } - - @Override - public ProducerRecord fromMessage(Message message, String defaultTopic) { - return this.converter.fromMessage(message, defaultTopic); - } - - }; - } - - @Bean - Function, Flux> uppercase() { - return s -> s.map(String::toUpperCase); - } - - @Bean - Function>, Flux> lowercase() { - return s -> s.map(rec -> new String(rec.value()).toLowerCase()); - } - - @Bean - java.util.function.Consumer> patternConsumer() { - return f -> f.doOnNext(s -> patternedDeliveries.add(s)) - .subscribe(); - } - - @Bean - ReceiverOptionsCustomizer cust1() { - return (t, u) -> { - recOptsCustOrder.add("one"); - return u; - }; - } - - @Bean - ReceiverOptionsCustomizer cust2() { - return new ReceiverOptionsCustomizer<>() { - - @Override - public ReceiverOptions apply(String t, ReceiverOptions u) { - recOptsCustOrder.add("two"); - u.withKeyDeserializer(new StringDeserializer()); - return u; - } - - @Override - public int getOrder() { - return -1; - } - - }; - } - - @Bean - FluxMessageChannel sendResults() { - return new FluxMessageChannel(); - } - - @ServiceActivator(inputChannel = "sendResults") - void handleResults(SenderResult result) { - if (result.exception() != null) { - failureFor(result); - } - else { - successFor(result); - } - } - - private void failureFor(SenderResult result) { - this.correlation.clear(); - } - - private boolean successFor(SenderResult result) { - return this.correlation.add(result.correlationMetadata()); - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderTests.java deleted file mode 100644 index 919691745..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/java/org/springframework/cloud/stream/binder/reactorkafka/ReactorKafkaBinderTests.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * 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.cloud.stream.binder.reactorkafka; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.RecordMetadata; -import org.junit.jupiter.api.Test; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.kafka.receiver.ReceiverOffset; -import reactor.kafka.sender.SenderResult; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.integration.config.ConsumerEndpointFactoryBean; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.KafkaHeaders; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; -import org.springframework.retry.support.RetryTemplate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * @author Gary Russell - * @since 4.0 - * - */ -@EmbeddedKafka(topics = { "testCa", "testCb", "testC1", "testP" }) -public class ReactorKafkaBinderTests { - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void consumerBinding() throws Exception { - KafkaProperties kafkaProperties = new KafkaProperties(); - kafkaProperties.setBootstrapServers( - Collections.singletonList(EmbeddedKafkaCondition.getBroker().getBrokersAsString())); - KafkaBinderConfigurationProperties binderProps = new KafkaBinderConfigurationProperties(kafkaProperties); - KafkaTopicProvisioner provisioner = new KafkaTopicProvisioner(binderProps, kafkaProperties, prop -> { - }); - provisioner.setMetadataRetryOperations(new RetryTemplate()); - ReactorKafkaBinder binder = new ReactorKafkaBinder(binderProps, provisioner); - binder.setApplicationContext(mock(GenericApplicationContext.class)); - - CountDownLatch latch = new CountDownLatch(2); - - FluxMessageChannel inbound = new FluxMessageChannel(); - Subscriber> sub = new Subscriber>() { - - @Override - public void onSubscribe(Subscription s) { - s.request(2); - } - - @Override - public void onNext(Message t) { - latch.countDown(); - } - - @Override - public void onError(Throwable t) { - } - - @Override - public void onComplete() { - } - - }; - inbound.subscribe(sub); - - KafkaConsumerProperties ext = new KafkaConsumerProperties(); - ExtendedConsumerProperties props = - new ExtendedConsumerProperties(ext); - props.setMultiplex(true); - - Binding consumer = binder.bindConsumer("testCa, testCb", "foo", inbound, props); - - DefaultKafkaProducerFactory pf = - new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(EmbeddedKafkaCondition.getBroker())); - KafkaTemplate kt = new KafkaTemplate<>(pf); - kt.send("testCa", "foo").get(10, TimeUnit.SECONDS); - kt.send("testCb", "bar").get(10, TimeUnit.SECONDS); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - consumer.unbind(); - pf.destroy(); - } - - @Test - void concurrencyManual() throws Exception { - concurrency(false); - } - - @Test - void concurrencyAtMostOnce() throws Exception { - concurrency(true); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - void concurrency(boolean atMostOnce) throws Exception { - KafkaProperties kafkaProperties = new KafkaProperties(); - kafkaProperties.setBootstrapServers( - Collections.singletonList(EmbeddedKafkaCondition.getBroker().getBrokersAsString())); - KafkaBinderConfigurationProperties binderProps = new KafkaBinderConfigurationProperties(kafkaProperties); - KafkaTopicProvisioner provisioner = new KafkaTopicProvisioner(binderProps, kafkaProperties, prop -> { - }); - provisioner.setMetadataRetryOperations(new RetryTemplate()); - ReactorKafkaBinder binder = new ReactorKafkaBinder(binderProps, provisioner); - binder.setApplicationContext(mock(GenericApplicationContext.class)); - - CountDownLatch subscriptionLatch = new CountDownLatch(1); - CountDownLatch messageLatch1 = new CountDownLatch(4); - CountDownLatch messageLatch2 = new CountDownLatch(6); - Set partitions = new HashSet<>(); - List payloads = Collections.synchronizedList(new ArrayList<>()); - - FluxMessageChannel inbound = new FluxMessageChannel(); - Subscriber> sub = new Subscriber>() { - - @Override - public void onSubscribe(Subscription s) { - s.request(10); - subscriptionLatch.countDown(); - } - - @Override - public void onNext(Message msg) { - payloads.add((String) msg.getPayload()); - partitions.add(msg.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION, Integer.class)); - if (!atMostOnce) { - msg.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, ReceiverOffset.class).acknowledge(); - } - messageLatch1.countDown(); - messageLatch2.countDown(); - } - - @Override - public void onError(Throwable t) { - } - - @Override - public void onComplete() { - } - - }; - inbound.subscribe(sub); - - KafkaConsumerProperties ext = new KafkaConsumerProperties(); - ext.setReactiveAtMostOnce(atMostOnce); - ExtendedConsumerProperties props = - new ExtendedConsumerProperties(ext); - props.setConcurrency(2); - - Binding consumer = binder.bindConsumer("testC1", "foo", inbound, props); - - assertThat(subscriptionLatch.await(10, TimeUnit.SECONDS)).isTrue(); - DefaultKafkaProducerFactory pf = - new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(EmbeddedKafkaCondition.getBroker())); - KafkaTemplate kt = new KafkaTemplate<>(pf); - kt.send("testC1", 0, null, "foo").get(10, TimeUnit.SECONDS); - kt.send("testC1", 1, null, "bar").get(10, TimeUnit.SECONDS); - kt.send("testC1", 0, null, "baz").get(10, TimeUnit.SECONDS); - kt.send("testC1", 1, null, "qux").get(10, TimeUnit.SECONDS); - assertThat(messageLatch1.await(10, TimeUnit.SECONDS)).isTrue(); - consumer.stop(); - consumer.start(); - kt.send("testC1", 0, null, "fiz").get(10, TimeUnit.SECONDS); - kt.send("testC1", 1, null, "buz").get(10, TimeUnit.SECONDS); - assertThat(messageLatch2.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(partitions).hasSize(2); - consumer.unbind(); - pf.destroy(); - Collections.sort(payloads); - if (!atMostOnce) { - assertThat(payloads).containsExactly("bar", "baz", "buz", "fiz", "foo", "qux"); - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void autoCommit() throws Exception { - KafkaProperties kafkaProperties = new KafkaProperties(); - kafkaProperties.setBootstrapServers( - Collections.singletonList(EmbeddedKafkaCondition.getBroker().getBrokersAsString())); - KafkaBinderConfigurationProperties binderProps = new KafkaBinderConfigurationProperties(kafkaProperties); - KafkaTopicProvisioner provisioner = new KafkaTopicProvisioner(binderProps, kafkaProperties, prop -> { - }); - provisioner.setMetadataRetryOperations(new RetryTemplate()); - ReactorKafkaBinder binder = new ReactorKafkaBinder(binderProps, provisioner); - binder.setApplicationContext(mock(GenericApplicationContext.class)); - - CountDownLatch subscriptionLatch = new CountDownLatch(1); - CountDownLatch messageLatch1 = new CountDownLatch(4); - Set partitions = new HashSet<>(); - List payloads = Collections.synchronizedList(new ArrayList<>()); - - FluxMessageChannel inbound = new FluxMessageChannel(); - Subscriber> sub = new Subscriber>() { - - @Override - public void onSubscribe(Subscription s) { - s.request(10); - subscriptionLatch.countDown(); - } - - @Override - public void onNext(Message msg) { - ((Message>>) msg).getPayload() - .doOnNext(rec -> { - payloads.add(rec.value()); - messageLatch1.countDown(); - }) - .subscribe(); - } - - @Override - public void onError(Throwable t) { - } - - @Override - public void onComplete() { - } - - }; - inbound.subscribe(sub); - - KafkaConsumerProperties ext = new KafkaConsumerProperties(); - ext.setReactiveAutoCommit(true); - ExtendedConsumerProperties props = - new ExtendedConsumerProperties(ext); - props.setConcurrency(2); - - Binding consumer = binder.bindConsumer("testC1", "foo", inbound, props); - - assertThat(subscriptionLatch.await(10, TimeUnit.SECONDS)).isTrue(); - DefaultKafkaProducerFactory pf = - new DefaultKafkaProducerFactory<>(KafkaTestUtils.producerProps(EmbeddedKafkaCondition.getBroker())); - KafkaTemplate kt = new KafkaTemplate<>(pf); - kt.send("testC1", 0, null, "foo").get(10, TimeUnit.SECONDS); - kt.send("testC1", 1, null, "bar").get(10, TimeUnit.SECONDS); - kt.send("testC1", 0, null, "baz").get(10, TimeUnit.SECONDS); - kt.send("testC1", 1, null, "qux").get(10, TimeUnit.SECONDS); - assertThat(messageLatch1.await(10, TimeUnit.SECONDS)).isTrue(); - consumer.unbind(); - pf.destroy(); - Collections.sort(payloads); - assertThat(payloads).containsExactly("bar", "baz", "foo", "qux"); - } - - @Test - void producerBinding() throws InterruptedException { - KafkaProperties kafkaProperties = new KafkaProperties(); - kafkaProperties.setBootstrapServers( - Collections.singletonList(EmbeddedKafkaCondition.getBroker().getBrokersAsString())); - KafkaBinderConfigurationProperties binderProps = new KafkaBinderConfigurationProperties(kafkaProperties); - KafkaTopicProvisioner provisioner = new KafkaTopicProvisioner(binderProps, kafkaProperties, prop -> { - }); - provisioner.setMetadataRetryOperations(new RetryTemplate()); - ReactorKafkaBinder binder = new ReactorKafkaBinder(binderProps, provisioner); - CountDownLatch latch = new CountDownLatch(1); - GenericApplicationContext context = new GenericApplicationContext(); - context.registerBean("sendResults", FluxMessageChannel.class); - context.refresh(); - FluxMessageChannel results = context.getBean("sendResults", FluxMessageChannel.class); - ConsumerEndpointFactoryBean fb = new ConsumerEndpointFactoryBean(); - AtomicReference> senderResult = new AtomicReference<>(); - fb.setHandler(new MessageHandler() { - - @SuppressWarnings("unchecked") - @Override - public void handleMessage(Message message) throws MessagingException { - senderResult.set((SenderResult) message.getPayload()); - latch.countDown(); - } - - }); - fb.setInputChannel(results); - fb.setBeanFactory(context.getBeanFactory()); - fb.afterPropertiesSet(); - fb.start(); - binder.setApplicationContext(context); - @SuppressWarnings("rawtypes") - ObjectProvider cust = mock(ObjectProvider.class); - AtomicBoolean custCalled = new AtomicBoolean(); - given(cust.getIfUnique()).willReturn((name, opts) -> { - custCalled.set(true); - return opts; - }); - binder.senderOptionsCustomizers(cust); - - MessageChannel outbound = new FluxMessageChannel(); - KafkaProducerProperties ext = new KafkaProducerProperties(); - ExtendedProducerProperties props = - new ExtendedProducerProperties(ext); - props.getExtension().setRecordMetadataChannel("sendResults"); - - Binding bindProducer = binder.bindProducer("testP", outbound, props); - AtomicReference> sendResult = new AtomicReference<>(); - outbound.send(MessageBuilder.withPayload("foo") - .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, 1) - .build()); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(senderResult.get().correlationMetadata()).isEqualTo(1); - bindProducer.unbind(); - assertThat(custCalled).isTrue(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/resources/logback.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/resources/logback.xml deleted file mode 100644 index 5fdac6f28..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-reactive/src/test/resources/logback.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - %d{ISO8601} %5p %t %c{2}:%L - %m%n - - - - - - - - - - - - - - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/.jdk8 b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/pom.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/pom.xml deleted file mode 100644 index c6b27579a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/pom.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-binder-kafka-streams - jar - spring-cloud-stream-binder-kafka-streams - Kafka Streams Binder Implementation - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-parent - 4.1.0-SNAPSHOT - - - - 1.8.2 - - - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-core - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-autoconfigure - true - - - org.apache.kafka - kafka-streams - - - org.springframework.kafka - spring-kafka - - - org.springframework.kafka - spring-kafka-test - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - org.awaitility - awaitility - test - - - org.springframework.integration - spring-integration-test - - - org.apache.avro - avro-compiler - - - test - - - org.springframework.cloud - spring-cloud-stream-binder-kafka - test - - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/AbstractKafkaStreamsBinderProcessor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/AbstractKafkaStreamsBinderProcessor.java deleted file mode 100644 index ba97e4057..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/AbstractKafkaStreamsBinderProcessor.java +++ /dev/null @@ -1,663 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.utils.Bytes; -import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.Topology; -import org.apache.kafka.streams.errors.LogAndContinueExceptionHandler; -import org.apache.kafka.streams.errors.LogAndFailExceptionHandler; -import org.apache.kafka.streams.kstream.Consumed; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.processor.TimestampExtractor; -import org.apache.kafka.streams.processor.api.Processor; -import org.apache.kafka.streams.processor.api.ProcessorContext; -import org.apache.kafka.streams.processor.api.ProcessorSupplier; -import org.apache.kafka.streams.processor.api.Record; -import org.apache.kafka.streams.processor.api.RecordMetadata; -import org.apache.kafka.streams.state.KeyValueStore; -import org.apache.kafka.streams.state.StoreBuilder; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.boot.context.properties.bind.BindContext; -import org.springframework.boot.context.properties.bind.BindHandler; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.boot.context.properties.source.ConfigurationPropertySources; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MutablePropertySources; -import org.springframework.integration.support.utils.IntegrationUtils; -import org.springframework.kafka.config.KafkaStreamsConfiguration; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.config.StreamsBuilderFactoryBeanConfigurer; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.streams.RecoveringDeserializationExceptionHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * @author Soby Chacko - * @since 3.0.0 - */ -public abstract class AbstractKafkaStreamsBinderProcessor implements ApplicationContextAware { - - private static final Log LOG = LogFactory.getLog(AbstractKafkaStreamsBinderProcessor.class); - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - private final BindingServiceProperties bindingServiceProperties; - - private final KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties; - - private final CleanupConfig cleanupConfig; - - private final KeyValueSerdeResolver keyValueSerdeResolver; - - protected ConfigurableApplicationContext applicationContext; - - public AbstractKafkaStreamsBinderProcessor(BindingServiceProperties bindingServiceProperties, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties, - KeyValueSerdeResolver keyValueSerdeResolver, CleanupConfig cleanupConfig) { - this.bindingServiceProperties = bindingServiceProperties; - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - this.kafkaStreamsExtendedBindingProperties = kafkaStreamsExtendedBindingProperties; - this.keyValueSerdeResolver = keyValueSerdeResolver; - this.cleanupConfig = cleanupConfig; - } - - @Override - public final void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; - } - - protected Topology.AutoOffsetReset getAutoOffsetReset(String inboundName, KafkaStreamsConsumerProperties extendedConsumerProperties) { - final KafkaConsumerProperties.StartOffset startOffset = extendedConsumerProperties - .getStartOffset(); - Topology.AutoOffsetReset autoOffsetReset = null; - if (startOffset != null) { - autoOffsetReset = switch (startOffset) { - case earliest -> Topology.AutoOffsetReset.EARLIEST; - case latest -> Topology.AutoOffsetReset.LATEST; - }; - } - if (extendedConsumerProperties.isResetOffsets()) { - AbstractKafkaStreamsBinderProcessor.LOG.warn("Detected resetOffsets configured on binding " - + inboundName + ". " - + "Setting resetOffsets in Kafka Streams binder does not have any effect."); - } - return autoOffsetReset; - } - - @SuppressWarnings("unchecked") - protected void handleKTableGlobalKTableInputs(Object[] arguments, int index, String input, Class parameterType, Object targetBean, - StreamsBuilderFactoryBean streamsBuilderFactoryBean, StreamsBuilder streamsBuilder, - KafkaStreamsConsumerProperties extendedConsumerProperties, - Serde keySerde, Serde valueSerde, Topology.AutoOffsetReset autoOffsetReset, boolean firstBuild) { - if (firstBuild) { - addStateStoreBeans(streamsBuilder); - } - if (parameterType.isAssignableFrom(KTable.class)) { - String materializedAs = extendedConsumerProperties.getMaterializedAs(); - String bindingDestination = this.bindingServiceProperties.getBindingDestination(input); - KTable table = getKTable(extendedConsumerProperties, streamsBuilder, keySerde, valueSerde, materializedAs, - bindingDestination, autoOffsetReset); - KTableBoundElementFactory.KTableWrapper kTableWrapper = - (KTableBoundElementFactory.KTableWrapper) targetBean; - //wrap the proxy created during the initial target type binding with real object (KTable) - kTableWrapper.wrap((KTable) table); - this.kafkaStreamsBindingInformationCatalogue.addStreamBuilderFactoryPerBinding(input, streamsBuilderFactoryBean); - this.kafkaStreamsBindingInformationCatalogue.addConsumerPropertiesPerSbfb(streamsBuilderFactoryBean, - bindingServiceProperties.getConsumerProperties(input)); - arguments[index] = table; - } - else if (parameterType.isAssignableFrom(GlobalKTable.class)) { - String materializedAs = extendedConsumerProperties.getMaterializedAs(); - String bindingDestination = this.bindingServiceProperties.getBindingDestination(input); - GlobalKTable table = getGlobalKTable(extendedConsumerProperties, streamsBuilder, keySerde, valueSerde, materializedAs, - bindingDestination, autoOffsetReset); - GlobalKTableBoundElementFactory.GlobalKTableWrapper globalKTableWrapper = - (GlobalKTableBoundElementFactory.GlobalKTableWrapper) targetBean; - //wrap the proxy created during the initial target type binding with real object (KTable) - globalKTableWrapper.wrap((GlobalKTable) table); - this.kafkaStreamsBindingInformationCatalogue.addStreamBuilderFactoryPerBinding(input, streamsBuilderFactoryBean); - this.kafkaStreamsBindingInformationCatalogue.addConsumerPropertiesPerSbfb(streamsBuilderFactoryBean, - bindingServiceProperties.getConsumerProperties(input)); - arguments[index] = table; - } - } - - @SuppressWarnings({ "unchecked" }) - protected StreamsBuilderFactoryBean buildStreamsBuilderAndRetrieveConfig(String beanNamePostPrefix, - ApplicationContext applicationContext, String inboundName, - KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties, - StreamsBuilderFactoryBeanConfigurer customizer, - ConfigurableEnvironment environment, BindingProperties bindingProperties) { - ConfigurableListableBeanFactory beanFactory = this.applicationContext - .getBeanFactory(); - - Map streamConfigGlobalProperties = applicationContext - .getBean("streamConfigGlobalProperties", Map.class); - - // Use a copy because the global configuration will be shared by multiple processors. - Map streamConfiguration = new HashMap<>(streamConfigGlobalProperties); - - if (kafkaStreamsBinderConfigurationProperties != null) { - final Map functionConfigMap = kafkaStreamsBinderConfigurationProperties.getFunctions(); - if (!CollectionUtils.isEmpty(functionConfigMap)) { - final KafkaStreamsBinderConfigurationProperties.Functions functionConfig = functionConfigMap.get(beanNamePostPrefix); - if (functionConfig != null) { - final Map functionSpecificConfig = functionConfig.getConfiguration(); - if (!CollectionUtils.isEmpty(functionSpecificConfig)) { - streamConfiguration.putAll(functionSpecificConfig); - } - - String applicationId = functionConfig.getApplicationId(); - if (StringUtils.hasText(applicationId)) { - streamConfiguration.put(StreamsConfig.APPLICATION_ID_CONFIG, applicationId); - } - } - } - } - - final MutablePropertySources propertySources = environment.getPropertySources(); - - if (StringUtils.hasText(bindingProperties.getBinder())) { - final KafkaStreamsBinderConfigurationProperties multiBinderKafkaStreamsBinderConfigurationProperties = - applicationContext.getBean(bindingProperties.getBinder() + "-KafkaStreamsBinderConfigurationProperties", KafkaStreamsBinderConfigurationProperties.class); - String connectionString = multiBinderKafkaStreamsBinderConfigurationProperties.getKafkaConnectionString(); - if (!StringUtils.hasText(connectionString)) { - connectionString = (String) propertySources.get(bindingProperties.getBinder() + "-kafkaStreamsBinderEnv").getProperty("spring.cloud.stream.kafka.binder.brokers"); - } - - streamConfiguration.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, connectionString); - - String binderProvidedApplicationId = multiBinderKafkaStreamsBinderConfigurationProperties.getApplicationId(); - if (StringUtils.hasText(binderProvidedApplicationId)) { - streamConfiguration.put(StreamsConfig.APPLICATION_ID_CONFIG, - binderProvidedApplicationId); - } - - if (multiBinderKafkaStreamsBinderConfigurationProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.logAndContinue) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - LogAndContinueExceptionHandler.class); - } - else if (multiBinderKafkaStreamsBinderConfigurationProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.logAndFail) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - LogAndFailExceptionHandler.class); - } - else if (multiBinderKafkaStreamsBinderConfigurationProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.sendToDlq) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - RecoveringDeserializationExceptionHandler.class); - SendToDlqAndContinue sendToDlqAndContinue = applicationContext.getBean(SendToDlqAndContinue.class); - streamConfiguration.put(RecoveringDeserializationExceptionHandler.KSTREAM_DESERIALIZATION_RECOVERER, sendToDlqAndContinue); - } - - if (!ObjectUtils.isEmpty(multiBinderKafkaStreamsBinderConfigurationProperties.getConfiguration())) { - streamConfiguration.putAll(multiBinderKafkaStreamsBinderConfigurationProperties.getConfiguration()); - } - if (!streamConfiguration.containsKey(StreamsConfig.REPLICATION_FACTOR_CONFIG)) { - streamConfiguration.put(StreamsConfig.REPLICATION_FACTOR_CONFIG, - (int) multiBinderKafkaStreamsBinderConfigurationProperties.getReplicationFactor()); - } - } - - //this is only used primarily for StreamListener based processors. Although in theory, functions can use it, - //it is ideal for functions to use the approach used in the above if statement by using a property like - //spring.cloud.stream.kafka.streams.binder.functions.process.configuration.num.threads (assuming that process is the function name). - KafkaStreamsConsumerProperties extendedConsumerProperties = this.kafkaStreamsExtendedBindingProperties - .getExtendedConsumerProperties(inboundName); - Map bindingConfig = extendedConsumerProperties.getConfiguration(); - Assert.state(!bindingConfig.containsKey(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG), - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG + " cannot be overridden at the binding level; " - + "use multiple binders instead"); - // We will only add the per binding configuration to the current streamConfiguration and not the global one. - streamConfiguration - .putAll(bindingConfig); - - String bindingLevelApplicationId = extendedConsumerProperties.getApplicationId(); - // override application.id if set at the individual binding level. - // We provide this for backward compatibility with StreamListener based processors. - // For function based processors see the approach used above - // (i.e. use a property like spring.cloud.stream.kafka.streams.binder.functions.process.applicationId). - if (StringUtils.hasText(bindingLevelApplicationId)) { - streamConfiguration.put(StreamsConfig.APPLICATION_ID_CONFIG, - bindingLevelApplicationId); - } - - //If the application id is not set by any mechanism, then generate it. - streamConfiguration.computeIfAbsent(StreamsConfig.APPLICATION_ID_CONFIG, - k -> { - String generatedApplicationID = beanNamePostPrefix + "-applicationId"; - LOG.info("Binder Generated Kafka Streams Application ID: " + generatedApplicationID); - LOG.info("Use the binder generated application ID only for development and testing. "); - LOG.info("For production deployments, please consider explicitly setting an application ID using a configuration property."); - LOG.info("The generated applicationID is static and will be preserved over application restarts."); - return generatedApplicationID; - }); - - handleConcurrency(applicationContext, inboundName, streamConfiguration); - - // Override deserialization exception handlers per binding - final DeserializationExceptionHandler deserializationExceptionHandler = - extendedConsumerProperties.getDeserializationExceptionHandler(); - if (deserializationExceptionHandler == DeserializationExceptionHandler.logAndFail) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - LogAndFailExceptionHandler.class); - } - else if (deserializationExceptionHandler == DeserializationExceptionHandler.logAndContinue) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - LogAndContinueExceptionHandler.class); - } - else if (deserializationExceptionHandler == DeserializationExceptionHandler.sendToDlq) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - RecoveringDeserializationExceptionHandler.class); - streamConfiguration.put(RecoveringDeserializationExceptionHandler.KSTREAM_DESERIALIZATION_RECOVERER, - applicationContext.getBean(SendToDlqAndContinue.class)); - } - else if (deserializationExceptionHandler == DeserializationExceptionHandler.skipAndContinue) { - streamConfiguration.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - SkipAndContinueExceptionHandler.class); - } - - KafkaStreamsConfiguration kafkaStreamsConfiguration = new KafkaStreamsConfiguration(streamConfiguration); - - StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.cleanupConfig == null - ? new StreamsBuilderFactoryBean(kafkaStreamsConfiguration) - : new StreamsBuilderFactoryBean(kafkaStreamsConfiguration, - this.cleanupConfig); - - streamsBuilderFactoryBean.setAutoStartup(false); - BeanDefinition streamsBuilderBeanDefinition = BeanDefinitionBuilder - .genericBeanDefinition( - (Class) streamsBuilderFactoryBean.getClass(), - () -> streamsBuilderFactoryBean) - .getRawBeanDefinition(); - ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition( - "stream-builder-" + beanNamePostPrefix, streamsBuilderBeanDefinition); - - extendedConsumerProperties.setApplicationId((String) streamConfiguration.get(StreamsConfig.APPLICATION_ID_CONFIG)); - - final StreamsBuilderFactoryBean streamsBuilderFactoryBeanFromContext = applicationContext.getBean( - "&stream-builder-" + beanNamePostPrefix, StreamsBuilderFactoryBean.class); - //At this point, the StreamsBuilderFactoryBean is created. If the users call, getObject() - //in the customizer, that should grant access to the StreamsBuilder. - if (customizer != null) { - customizer.configure(streamsBuilderFactoryBean); - } - return streamsBuilderFactoryBeanFromContext; - } - - private void handleConcurrency(ApplicationContext applicationContext, String inboundName, - Map streamConfiguration) { - // This rebinding is necessary to capture the concurrency explicitly set by the application. - // This is added to fix this issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/899 - org.springframework.boot.context.properties.bind.Binder explicitConcurrencyResolver = - new org.springframework.boot.context.properties.bind.Binder(ConfigurationPropertySources.get(applicationContext.getEnvironment()), - new PropertySourcesPlaceholdersResolver(applicationContext.getEnvironment()), - IntegrationUtils.getConversionService(this.applicationContext.getBeanFactory()), null); - - boolean[] concurrencyExplicitlyProvided = new boolean[] {false}; - BindHandler handler = new BindHandler() { - - @Override - public Object onSuccess(ConfigurationPropertyName name, Bindable target, - BindContext context, Object result) { - if (!concurrencyExplicitlyProvided[0]) { - concurrencyExplicitlyProvided[0] = name.getLastElement(ConfigurationPropertyName.Form.UNIFORM).equals("concurrency") && - // name is normalized to contain only uniform elements and thus safe to call toLowerCase here. - ConfigurationPropertyName.of("spring.cloud.stream.bindings." + inboundName.toLowerCase() + ".consumer") - .isAncestorOf(name); - } - return result; - } - }; - //Re-bind spring.cloud.stream properties to check if the application explicitly provided concurrency. - try { - explicitConcurrencyResolver.bind("spring.cloud.stream", - Bindable.ofInstance(new BindingServiceProperties()), handler); - } - catch (Exception e) { - // Ignore this exception - } - - int concurrency = this.bindingServiceProperties.getConsumerProperties(inboundName) - .getConcurrency(); - // override concurrency if set at the individual binding level. - // Concurrency will be mapped to num.stream.threads. - // This conditional also takes into account explicit concurrency settings left at the default value of 1 - // by the application to address concurrency behavior in applications with multiple processors. - // See this GH issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/844 - if (concurrency >= 1 && concurrencyExplicitlyProvided[0]) { - streamConfiguration.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, - concurrency); - } - } - - protected Serde getValueSerde(String inboundName, KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, ResolvableType resolvableType) { - if (bindingServiceProperties.getConsumerProperties(inboundName).isUseNativeDecoding()) { - BindingProperties bindingProperties = this.bindingServiceProperties - .getBindingProperties(inboundName); - return this.keyValueSerdeResolver.getInboundValueSerde( - bindingProperties.getConsumer(), kafkaStreamsConsumerProperties, resolvableType); - } - else { - return Serdes.ByteArray(); - } - } - - @SuppressWarnings({"unchecked"}) - protected KStream getKStream(String inboundName, BindingProperties bindingProperties, KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, - StreamsBuilder streamsBuilder, Serde keySerde, Serde valueSerde, Topology.AutoOffsetReset autoOffsetReset, boolean firstBuild) { - if (firstBuild) { - addStateStoreBeans(streamsBuilder); - } - - final boolean nativeDecoding = this.bindingServiceProperties - .getConsumerProperties(inboundName).isUseNativeDecoding(); - if (nativeDecoding) { - LOG.info("Native decoding is enabled for " + inboundName - + ". Inbound deserialization done at the broker."); - } - else { - LOG.info("Native decoding is disabled for " + inboundName - + ". Inbound message conversion done by Spring Cloud Stream."); - } - - KStream stream; - final Serde valueSerdeToUse = getValueSerdeToUse(kafkaStreamsConsumerProperties, valueSerde); - final Consumed consumed = getConsumed(kafkaStreamsConsumerProperties, keySerde, valueSerdeToUse, autoOffsetReset); - - if (this.kafkaStreamsExtendedBindingProperties - .getExtendedConsumerProperties(inboundName).isDestinationIsPattern()) { - final Pattern pattern = Pattern.compile(this.bindingServiceProperties.getBindingDestination(inboundName)); - - stream = streamsBuilder.stream(pattern, consumed); - } - else { - String[] bindingTargets = StringUtils.commaDelimitedListToStringArray( - this.bindingServiceProperties.getBindingDestination(inboundName)); - stream = streamsBuilder.stream(Arrays.asList(bindingTargets), - consumed); - } - //Check to see if event type based routing is enabled. - //See this issue for more context: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1003 - if (StringUtils.hasText(kafkaStreamsConsumerProperties.getEventTypes())) { - AtomicBoolean matched = new AtomicBoolean(); - AtomicReference topicObject = new AtomicReference<>(); - AtomicReference headersObject = new AtomicReference<>(); - // Processor to retrieve the header value. - stream.process(() -> eventTypeProcessor(kafkaStreamsConsumerProperties, matched, topicObject, headersObject)); - // Branching based on event type match. - final KStream[] branch = stream.branch((key, value) -> matched.getAndSet(false)); - // Deserialize if we have a branch from above. - final KStream deserializedKStream = !kafkaStreamsConsumerProperties.isUseConfiguredSerdeWhenRoutingEvents() ? - branch[0].mapValues(value -> valueSerde.deserializer().deserialize( - topicObject.get(), headersObject.get(), ((Bytes) value).get())) : (KStream) branch[0]; - return getkStream(bindingProperties, deserializedKStream, nativeDecoding); - } - return getkStream(bindingProperties, stream, nativeDecoding); - } - - private Serde getValueSerdeToUse(KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, Serde valueSerde) { - if (StringUtils.hasText(kafkaStreamsConsumerProperties.getEventTypes())) { - return kafkaStreamsConsumerProperties.isUseConfiguredSerdeWhenRoutingEvents() ? valueSerde : new Serdes.BytesSerde(); - } - return valueSerde; - } - - private KStream getkStream(BindingProperties bindingProperties, KStream stream, boolean nativeDecoding) { - if (!nativeDecoding) { - AtomicReference headersAtomicReference = new AtomicReference<>(); - stream.process((ProcessorSupplier) () -> new Processor() { - - @Override - public void init(ProcessorContext context) { - Processor.super.init(context); - } - - @Override - public void process(Record record) { - final Headers headers = record.headers(); - headersAtomicReference.set(headers); - } - - @Override - public void close() { - Processor.super.close(); - } - }); - stream = stream.mapValues((value) -> { - Object returnValue; - String contentType = bindingProperties.getContentType(); - if (value != null && StringUtils.hasText(contentType)) { - final Headers headers = headersAtomicReference.get(); - final Map headersMap = new HashMap<>(); - headers.forEach(header -> headersMap.put(header.key(), header.value())); - returnValue = MessageBuilder.withPayload(value).copyHeaders(headersMap) - .setHeader(MessageHeaders.CONTENT_TYPE, contentType).build(); - } - else { - returnValue = value; - } - return returnValue; - }); - } - return stream; - } - - @SuppressWarnings("rawtypes") - private void addStateStoreBeans(StreamsBuilder streamsBuilder) { - try { - final Map storeBuilders = applicationContext.getBeansOfType(StoreBuilder.class); - if (!CollectionUtils.isEmpty(storeBuilders)) { - storeBuilders.values().forEach(storeBuilder -> { - streamsBuilder.addStateStore(storeBuilder); - if (LOG.isInfoEnabled()) { - LOG.info("state store " + storeBuilder.name() + " added to topology"); - } - }); - } - } - catch (Exception e) { - // Pass through. - } - } - - private KTable materializedAs(StreamsBuilder streamsBuilder, String destination, String storeName, - Serde k, Serde v, Topology.AutoOffsetReset autoOffsetReset, KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties) { - - final Consumed consumed = getConsumed(kafkaStreamsConsumerProperties, k, v, autoOffsetReset); - return streamsBuilder.table(this.bindingServiceProperties.getBindingDestination(destination), - consumed, getMaterialized(storeName, k, v)); - } - - private Materialized> getMaterialized( - String storeName, Serde k, Serde v) { - return Materialized.>as(storeName) - .withKeySerde(k).withValueSerde(v); - } - - private GlobalKTable materializedAsGlobalKTable( - StreamsBuilder streamsBuilder, String destination, String storeName, - Serde k, Serde v, Topology.AutoOffsetReset autoOffsetReset, KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties) { - final Consumed consumed = getConsumed(kafkaStreamsConsumerProperties, k, v, autoOffsetReset); - return streamsBuilder.globalTable( - this.bindingServiceProperties.getBindingDestination(destination), - consumed, - getMaterialized(storeName, k, v)); - } - - private GlobalKTable getGlobalKTable(KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, - StreamsBuilder streamsBuilder, - Serde keySerde, Serde valueSerde, String materializedAs, - String bindingDestination, Topology.AutoOffsetReset autoOffsetReset) { - final Consumed consumed = getConsumed(kafkaStreamsConsumerProperties, keySerde, valueSerde, autoOffsetReset); - return materializedAs != null - ? materializedAsGlobalKTable(streamsBuilder, bindingDestination, - materializedAs, keySerde, valueSerde, autoOffsetReset, kafkaStreamsConsumerProperties) - : streamsBuilder.globalTable(bindingDestination, - consumed); - } - - @SuppressWarnings({"unchecked"}) - private KTable getKTable(KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, - StreamsBuilder streamsBuilder, Serde keySerde, - Serde valueSerde, String materializedAs, String bindingDestination, - Topology.AutoOffsetReset autoOffsetReset) { - - final Serde valueSerdeToUse = getValueSerdeToUse(kafkaStreamsConsumerProperties, valueSerde); - - final Consumed consumed = getConsumed(kafkaStreamsConsumerProperties, keySerde, valueSerdeToUse, autoOffsetReset); - - final KTable kTable = materializedAs != null - ? materializedAs(streamsBuilder, bindingDestination, materializedAs, - keySerde, valueSerdeToUse, autoOffsetReset, kafkaStreamsConsumerProperties) - : streamsBuilder.table(bindingDestination, - consumed); - if (StringUtils.hasText(kafkaStreamsConsumerProperties.getEventTypes())) { - AtomicBoolean matched = new AtomicBoolean(); - AtomicReference topicObject = new AtomicReference<>(); - AtomicReference headersObject = new AtomicReference<>(); - - final KStream stream = kTable.toStream(); - - // Processor to retrieve the header value. - stream.process(() -> eventTypeProcessor(kafkaStreamsConsumerProperties, matched, topicObject, headersObject)); - // Branching based on event type match. - final Map> stringKStreamMap = stream.split() - .branch((key, value) -> matched.getAndSet(false)) - .noDefaultBranch(); - final KStream[] branch = stringKStreamMap.values().toArray(new KStream[0]); - // Deserialize if we have a branch from above. - final KStream deserializedKStream = !kafkaStreamsConsumerProperties.isUseConfiguredSerdeWhenRoutingEvents() ? - branch[0].mapValues(value -> valueSerde.deserializer().deserialize( - topicObject.get(), headersObject.get(), ((Bytes) value).get())) : (KStream) branch[0]; - - return deserializedKStream.toTable(); - } - return kTable; - } - - private Consumed getConsumed(KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, - Serde keySerde, Serde valueSerde, Topology.AutoOffsetReset autoOffsetReset) { - TimestampExtractor timestampExtractor = null; - if (StringUtils.hasText(kafkaStreamsConsumerProperties.getTimestampExtractorBeanName())) { - timestampExtractor = applicationContext.getBean(kafkaStreamsConsumerProperties.getTimestampExtractorBeanName(), - TimestampExtractor.class); - } - final Consumed consumed = Consumed.with(keySerde, valueSerde) - .withOffsetResetPolicy(autoOffsetReset); - if (timestampExtractor != null) { - consumed.withTimestampExtractor(timestampExtractor); - } - if (StringUtils.hasText(kafkaStreamsConsumerProperties.getConsumedAs())) { - consumed.withName(kafkaStreamsConsumerProperties.getConsumedAs()); - } - return consumed; - } - - private Processor eventTypeProcessor(KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties, - AtomicBoolean matched, AtomicReference topicObject, AtomicReference headersObject) { - return new Processor<>() { - - org.apache.kafka.streams.processor.api.ProcessorContext context; - - @Override - public void init(org.apache.kafka.streams.processor.api.ProcessorContext context) { - Processor.super.init(context); - this.context = context; - } - - @Override - public void process(Record record) { - final Headers headers = record.headers(); - headersObject.set(headers); - final Optional optional = this.context.recordMetadata(); - if (optional.isPresent()) { - final RecordMetadata recordMetadata = optional.get(); - topicObject.set(recordMetadata.topic()); - } - final Iterable

eventTypeHeader = headers.headers(kafkaStreamsConsumerProperties.getEventTypeHeaderKey()); - if (eventTypeHeader != null && eventTypeHeader.iterator().hasNext()) { - String eventTypeFromHeader = new String(eventTypeHeader.iterator().next().value()); - final String[] eventTypesFromBinding = StringUtils.commaDelimitedListToStringArray(kafkaStreamsConsumerProperties.getEventTypes()); - for (String eventTypeFromBinding : eventTypesFromBinding) { - if (eventTypeFromHeader.equals(eventTypeFromBinding)) { - matched.set(true); - break; - } - } - } - } - - @Override - public void close() { - - } - }; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DeserializationExceptionHandler.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DeserializationExceptionHandler.java deleted file mode 100644 index 57e1a3322..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DeserializationExceptionHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams; - -/** - * Enumeration for various {@link org.apache.kafka.streams.errors.DeserializationExceptionHandler} types. - * - * @author Soby Chacko - * @since 3.0.0 - */ -public enum DeserializationExceptionHandler { - - /** - * Deserialization error handler with log and continue. - * See {@link org.apache.kafka.streams.errors.LogAndContinueExceptionHandler} - */ - logAndContinue, - /** - * Deserialization error handler with log and fail. - * See {@link org.apache.kafka.streams.errors.LogAndFailExceptionHandler} - */ - logAndFail, - /** - * Deserialization error handler with DLQ send. - * See {@link org.springframework.kafka.streams.RecoveringDeserializationExceptionHandler} - */ - sendToDlq, - /** - * Deserialization error handler that silently skips the error and continue. - */ - skipAndContinue; - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DltAwareProcessor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DltAwareProcessor.java deleted file mode 100644 index 5cdbbbd1a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DltAwareProcessor.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Supplier; - -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.processor.api.Processor; -import org.apache.kafka.streams.processor.api.ProcessorContext; -import org.apache.kafka.streams.processor.api.Record; - -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Custom {@link Processor} implementation that is capable of sending a record - * to a DLT if the processing fails. - * - * @author Soby Chacko - * @since 4.1.0 - */ -public class DltAwareProcessor implements Processor { - - /** - * Delegate {@link BiFunction} that is responsible for processing the data. - */ - private final BiFunction> delegateFunction; - - /** - * Event time for the forwarded downstream record. - */ - private final Supplier recordTimeSupplier; - - /** - * DLT destination. - */ - private String dltDestination; - - /** - * {@link DltPublishingContext} used for DLT publishing needs. - */ - private DltPublishingContext dltPublishingContext; - - /** - * A {@link BiConsumer} that does the recovery of a failed record. - */ - private BiConsumer, Exception> processorRecordRecoverer; - - /** - * {@link ProcessorContext} used in the processor. - */ - private ProcessorContext context; - - /** - * - * @param delegateFunction {@link BiFunction} to process the data - * @param dltDestination DLT destination - * @param dltPublishingContext {@link DltPublishingContext} - */ - public DltAwareProcessor(BiFunction> delegateFunction, String dltDestination, - DltPublishingContext dltPublishingContext) { - this(delegateFunction, dltDestination, dltPublishingContext, System::currentTimeMillis); - } - - /** - * - * @param delegateFunction {@link BiFunction} to process the data - * @param dltDestination DLT destination - * @param dltPublishingContext {@link DltPublishingContext} - * @param recordTimeSupplier Supplier for downstream record timestamp - */ - public DltAwareProcessor(BiFunction> delegateFunction, String dltDestination, - DltPublishingContext dltPublishingContext, Supplier recordTimeSupplier) { - this.delegateFunction = delegateFunction; - this.recordTimeSupplier = recordTimeSupplier; - Assert.isTrue(StringUtils.hasText(dltDestination), "DLT Destination topic must be provided."); - this.dltDestination = dltDestination; - Assert.notNull(dltPublishingContext, "DltSenderContext cannot be null"); - this.dltPublishingContext = dltPublishingContext; - } - - /** - * - * @param delegateFunction {@link BiFunction} to process the data - * @param processorRecordRecoverer {@link BiConsumer} that recovers failed records - */ - public DltAwareProcessor(BiFunction> delegateFunction, - BiConsumer, Exception> processorRecordRecoverer) { - this(delegateFunction, processorRecordRecoverer, System::currentTimeMillis); - } - - /** - * - * @param delegateFunction {@link BiFunction} to process the data - * @param processorRecordRecoverer {@link BiConsumer} that recovers failed records - * @param recordTimeSupplier Supplier for downstream record timestamp - */ - public DltAwareProcessor(BiFunction> delegateFunction, - BiConsumer, Exception> processorRecordRecoverer, Supplier recordTimeSupplier) { - this.delegateFunction = delegateFunction; - this.recordTimeSupplier = recordTimeSupplier; - Assert.notNull(processorRecordRecoverer, "You must provide a valid processor recoverer"); - this.processorRecordRecoverer = processorRecordRecoverer; - } - - @Override - public void init(ProcessorContext context) { - Processor.super.init(context); - this.context = context; - } - - @Override - public void process(Record record) { - try { - KeyValue keyValue = this.delegateFunction.apply(record.key(), record.value()); - Record downstreamRecord = new Record<>(keyValue.key, keyValue.value, recordTimeSupplier.get(), record.headers()); - this.context.forward(downstreamRecord); - } - catch (Exception exception) { - if (this.processorRecordRecoverer == null) { - this.processorRecordRecoverer = defaultProcessorRecordRecoverer(); - } - this.processorRecordRecoverer.accept(record, exception); - } - } - - @Override - public void close() { - Processor.super.close(); - } - - BiConsumer, Exception> defaultProcessorRecordRecoverer() { - return (r, e) -> { - StreamBridge streamBridge = this.dltPublishingContext.getStreamBridge(); - if (streamBridge != null) { - streamBridge.send(this.dltDestination, r.value()); - } - }; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DltPublishingContext.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DltPublishingContext.java deleted file mode 100644 index 641449eb9..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/DltPublishingContext.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; - -/** - * The DltPublishingContext is meant to be used along with {@link DltAwareProcessor} - * when publishing failed record to a DLT. DltPublishingContext is particularly used - * for accessing framework beans such as {@link StreamBridge}. - * - * @author Soby Chacko - */ -public class DltPublishingContext implements ApplicationContextAware, InitializingBean { - - private ConfigurableApplicationContext applicationContext; - - private StreamBridge streamBridge; - - @Override - public void afterPropertiesSet() { - this.streamBridge = applicationContext.getBean(StreamBridge.class); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; - } - - public StreamBridge getStreamBridge() { - return this.streamBridge; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/EncodingDecodingBindAdviceHandler.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/EncodingDecodingBindAdviceHandler.java deleted file mode 100644 index 7ef9d37bc..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/EncodingDecodingBindAdviceHandler.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams; - -import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; -import org.springframework.boot.context.properties.bind.AbstractBindHandler; -import org.springframework.boot.context.properties.bind.BindContext; -import org.springframework.boot.context.properties.bind.BindHandler; -import org.springframework.boot.context.properties.bind.BindResult; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; - -/** - * {@link ConfigurationPropertiesBindHandlerAdvisor} to detect nativeEncoding/Decoding settings - * provided by the application explicitly. - * - * @author Soby Chacko - * @since 3.0.0 - */ -public class EncodingDecodingBindAdviceHandler implements ConfigurationPropertiesBindHandlerAdvisor { - - private boolean encodingSettingProvided; - private boolean decodingSettingProvided; - - public boolean isDecodingSettingProvided() { - return this.decodingSettingProvided; - } - - public boolean isEncodingSettingProvided() { - return this.encodingSettingProvided; - } - - @Override - public BindHandler apply(BindHandler bindHandler) { - return new AbstractBindHandler(bindHandler) { - @Override - public Bindable onStart(ConfigurationPropertyName name, - Bindable target, BindContext context) { - final String configName = name.toString(); - if (configName.contains("use") && configName.contains("native") && - (configName.contains("encoding") || configName.contains("decoding"))) { - BindResult result = context.getBinder().bind(name, target); - if (result.isBound()) { - if (configName.contains("encoding")) { - EncodingDecodingBindAdviceHandler.this.encodingSettingProvided = true; - } - else { - EncodingDecodingBindAdviceHandler.this.decodingSettingProvided = true; - } - return target.withExistingValue(result.get()); - } - } - return bindHandler.onStart(name, target, context); - } - }; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/ExtendedBindingHandlerMappingsProviderAutoConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/ExtendedBindingHandlerMappingsProviderAutoConfiguration.java deleted file mode 100644 index a613ad555..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/ExtendedBindingHandlerMappingsProviderAutoConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.cloud.stream.config.BindingHandlerAdvise.MappingsProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for extended binding metadata for Kafka Streams. - * - * @author Chris Bono - * @since 3.2 - */ -@Configuration(proxyBeanMethods = false) -public class ExtendedBindingHandlerMappingsProviderAutoConfiguration { - - @Bean - public MappingsProvider kafkaStreamsExtendedPropertiesDefaultMappingsProvider() { - return () -> { - Map mappings = new HashMap<>(); - mappings.put( - ConfigurationPropertyName.of("spring.cloud.stream.kafka.streams"), - ConfigurationPropertyName.of("spring.cloud.stream.kafka.streams.default")); - mappings.put( - ConfigurationPropertyName.of("spring.cloud.stream.kafka.streams.bindings"), - ConfigurationPropertyName.of("spring.cloud.stream.kafka.streams.default")); - return mappings; - }; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBinder.java deleted file mode 100644 index 7269875da..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBinder.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.GlobalKTable; - -import org.springframework.cloud.stream.binder.AbstractBinder; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.DefaultBinding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsProducerProperties; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.StringUtils; - -/** - * An {@link AbstractBinder} implementation for {@link GlobalKTable}. - *

- * Provides only consumer binding for the bound {@link GlobalKTable}. Output bindings are - * not allowed on this binder. - * - * @author Soby Chacko - * @since 2.1.0 - */ -public class GlobalKTableBinder extends - // @checkstyle:off - AbstractBinder, ExtendedConsumerProperties, ExtendedProducerProperties> - implements - ExtendedPropertiesBinder, KafkaStreamsConsumerProperties, KafkaStreamsProducerProperties> { - - // @checkstyle:on - private final KafkaStreamsBinderConfigurationProperties binderConfigurationProperties; - - private final KafkaTopicProvisioner kafkaTopicProvisioner; - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - // @checkstyle:off - private KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties = new KafkaStreamsExtendedBindingProperties(); - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - // @checkstyle:on - - public GlobalKTableBinder( - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, KafkaStreamsRegistry kafkaStreamsRegistry) { - this.binderConfigurationProperties = binderConfigurationProperties; - this.kafkaTopicProvisioner = kafkaTopicProvisioner; - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - } - - @Override - @SuppressWarnings("unchecked") - protected Binding> doBindConsumer(String name, - String group, GlobalKTable inputTarget, - ExtendedConsumerProperties properties) { - if (!StringUtils.hasText(group)) { - group = properties.getExtension().getApplicationId(); - } - final RetryTemplate retryTemplate = buildRetryTemplate(properties); - - final String bindingName = this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(inputTarget); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeanPerBinding().get(bindingName); - - KafkaStreamsBinderUtils.prepareConsumerBinding(name, group, - getApplicationContext(), this.kafkaTopicProvisioner, - this.binderConfigurationProperties, properties, retryTemplate, getBeanFactory(), - this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(inputTarget), - this.kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - - return new DefaultBinding>(bindingName, group, inputTarget, streamsBuilderFactoryBean) { - - @Override - public boolean isInput() { - return true; - } - - @Override - public synchronized void start() { - if (!streamsBuilderFactoryBean.isRunning()) { - super.start(); - GlobalKTableBinder.this.kafkaStreamsRegistry.registerKafkaStreams(streamsBuilderFactoryBean); - //If we cached the previous KafkaStreams object (from a binding stop on the actuator), remove it. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - final String applicationId = (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG); - if (kafkaStreamsBindingInformationCatalogue.getStoppedKafkaStreams().containsKey(applicationId)) { - kafkaStreamsBindingInformationCatalogue.removePreviousKafkaStreamsForApplicationId(applicationId); - } - } - } - - @Override - public synchronized void stop() { - if (streamsBuilderFactoryBean.isRunning()) { - final KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - super.stop(); - GlobalKTableBinder.this.kafkaStreamsRegistry.unregisterKafkaStreams(kafkaStreams); - KafkaStreamsBinderUtils.closeDlqProducerFactories(kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - //Caching the stopped KafkaStreams for health indicator purposes on the underlying processor. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - GlobalKTableBinder.this.kafkaStreamsBindingInformationCatalogue.addPreviousKafkaStreamsForApplicationId( - (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG), kafkaStreams); - } - } - }; - } - - @Override - protected Binding> doBindProducer(String name, - GlobalKTable outboundBindTarget, - ExtendedProducerProperties properties) { - throw new UnsupportedOperationException( - "No producer level binding is allowed for GlobalKTable"); - } - - @Override - public KafkaStreamsConsumerProperties getExtendedConsumerProperties( - String channelName) { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedConsumerProperties(channelName); - } - - @Override - public KafkaStreamsProducerProperties getExtendedProducerProperties( - String channelName) { - throw new UnsupportedOperationException( - "No producer binding is allowed and therefore no properties"); - } - - @Override - public String getDefaultsPrefix() { - return this.kafkaStreamsExtendedBindingProperties.getDefaultsPrefix(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedPropertiesEntryClass(); - } - - public void setKafkaStreamsExtendedBindingProperties( - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties) { - this.kafkaStreamsExtendedBindingProperties = kafkaStreamsExtendedBindingProperties; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBinderConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBinderConfiguration.java deleted file mode 100644 index 8a6cc6d55..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBinderConfiguration.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams; - -import java.util.Map; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * Configuration for GlobalKTable binder. - * - * @author Soby Chacko - * @author Chris Bono - * @since 2.1.0 - */ -@Configuration(proxyBeanMethods = false) -@Import({ MultiBinderPropertiesConfiguration.class, - KafkaStreamsBinderHealthIndicatorConfiguration.class, - KafkaStreamsJaasConfiguration.class}) -@EnableConfigurationProperties(KafkaProperties.class) -public class GlobalKTableBinderConfiguration { - - @Bean - public KafkaTopicProvisioner provisioningProvider( - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaProperties kafkaProperties, ObjectProvider adminClientConfigCustomizer) { - return new KafkaTopicProvisioner(binderConfigurationProperties, kafkaProperties, adminClientConfigCustomizer.getIfUnique()); - } - - @Bean - public GlobalKTableBinder GlobalKTableBinder( - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - @Qualifier("streamConfigGlobalProperties") Map streamConfigGlobalProperties, - KafkaStreamsRegistry kafkaStreamsRegistry) { - - GlobalKTableBinder globalKTableBinder = new GlobalKTableBinder(binderConfigurationProperties, - kafkaTopicProvisioner, kafkaStreamsBindingInformationCatalogue, kafkaStreamsRegistry); - globalKTableBinder.setKafkaStreamsExtendedBindingProperties( - kafkaStreamsExtendedBindingProperties); - return globalKTableBinder; - } - - @Bean - @ConditionalOnBean(name = "outerContext") - public static BeanFactoryPostProcessor outerContextBeanFactoryPostProcessor() { - return beanFactory -> { - - // It is safe to call getBean("outerContext") here, because this bean is - // registered as first - // and as independent from the parent context. - ApplicationContext outerContext = (ApplicationContext) beanFactory - .getBean("outerContext"); - beanFactory.registerSingleton( - KafkaStreamsExtendedBindingProperties.class.getSimpleName(), - outerContext.getBean(KafkaStreamsExtendedBindingProperties.class)); - beanFactory.registerSingleton( - KafkaStreamsBindingInformationCatalogue.class.getSimpleName(), - outerContext.getBean(KafkaStreamsBindingInformationCatalogue.class)); - beanFactory.registerSingleton( - KafkaStreamsRegistry.class.getSimpleName(), - outerContext.getBean(KafkaStreamsRegistry.class)); - }; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBoundElementFactory.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBoundElementFactory.java deleted file mode 100644 index b7d125b57..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/GlobalKTableBoundElementFactory.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.kafka.streams.kstream.GlobalKTable; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binding.AbstractBindingTargetFactory; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.util.Assert; - -/** - * {@link org.springframework.cloud.stream.binding.BindingTargetFactory} for - * {@link GlobalKTable} - * - * Input bindings are only created as output bindings on GlobalKTable are not allowed. - * - * @author Soby Chacko - * @since 2.1.0 - */ -public class GlobalKTableBoundElementFactory - extends AbstractBindingTargetFactory { - - private final BindingServiceProperties bindingServiceProperties; - private final EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler; - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - GlobalKTableBoundElementFactory(BindingServiceProperties bindingServiceProperties, - EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue) { - super(GlobalKTable.class); - this.bindingServiceProperties = bindingServiceProperties; - this.encodingDecodingBindAdviceHandler = encodingDecodingBindAdviceHandler; - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - } - - @Override - public GlobalKTable createInput(String name) { - BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(name); - ConsumerProperties consumerProperties = bindingProperties.getConsumer(); - if (consumerProperties == null) { - consumerProperties = this.bindingServiceProperties.getConsumerProperties(name); - consumerProperties.setUseNativeDecoding(true); - } - else { - if (!encodingDecodingBindAdviceHandler.isDecodingSettingProvided()) { - consumerProperties.setUseNativeDecoding(true); - } - } - // Always set multiplex to true in the kafka streams binder - consumerProperties.setMultiplex(true); - - // @checkstyle:off - GlobalKTableBoundElementFactory.GlobalKTableWrapperHandler wrapper = new GlobalKTableBoundElementFactory.GlobalKTableWrapperHandler(); - // @checkstyle:on - ProxyFactory proxyFactory = new ProxyFactory( - GlobalKTableBoundElementFactory.GlobalKTableWrapper.class, - GlobalKTable.class); - proxyFactory.addAdvice(wrapper); - - final GlobalKTable proxy = (GlobalKTable) proxyFactory.getProxy(); - this.kafkaStreamsBindingInformationCatalogue.addBindingNamePerTarget(proxy, name); - return proxy; - } - - @Override - public GlobalKTable createOutput(String name) { - throw new UnsupportedOperationException( - "Outbound operations are not allowed on target type GlobalKTable"); - } - - /** - * Wrapper for GlobalKTable proxy. - */ - public interface GlobalKTableWrapper { - - void wrap(GlobalKTable delegate); - - } - - private static class GlobalKTableWrapperHandler implements - GlobalKTableBoundElementFactory.GlobalKTableWrapper, MethodInterceptor { - - private GlobalKTable delegate; - - public void wrap(GlobalKTable delegate) { - Assert.notNull(delegate, "delegate cannot be null"); - if (this.delegate == null) { - this.delegate = delegate; - } - } - - @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { - if (methodInvocation.getMethod().getDeclaringClass() - .equals(GlobalKTable.class)) { - Assert.notNull(this.delegate, - "Trying to prepareConsumerBinding " + methodInvocation.getMethod() - + " but no delegate has been set."); - return methodInvocation.getMethod().invoke(this.delegate, - methodInvocation.getArguments()); - } - else if (methodInvocation.getMethod().getDeclaringClass() - .equals(GlobalKTableBoundElementFactory.GlobalKTableWrapper.class)) { - return methodInvocation.getMethod().invoke(this, - methodInvocation.getArguments()); - } - else { - throw new IllegalStateException( - "Only GlobalKTable method invocations are permitted"); - } - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/InteractiveQueryService.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/InteractiveQueryService.java deleted file mode 100644 index 2065ee3c2..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/InteractiveQueryService.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.common.serialization.Serializer; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.KeyQueryMetadata; -import org.apache.kafka.streams.StoreQueryParameters; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.errors.InvalidStateStoreException; -import org.apache.kafka.streams.errors.UnknownStateStoreException; -import org.apache.kafka.streams.state.HostInfo; -import org.apache.kafka.streams.state.QueryableStoreType; -import org.apache.kafka.streams.state.StreamsMetadata; - -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.retry.RetryPolicy; -import org.springframework.retry.backoff.FixedBackOffPolicy; -import org.springframework.retry.policy.SimpleRetryPolicy; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.StringUtils; - -/** - * Services pertinent to the interactive query capabilities of Kafka Streams. This class - * provides services such as querying for a particular store, which instance is hosting a - * particular store etc. This is part of the public API of the kafka streams binder and - * the users can inject this service in their applications to make use of it. - * - * @author Soby Chacko - * @author Renwei Han - * @author Serhii Siryi - * @author Nico Pommerening - * @author Chris Bono - * @since 2.1.0 - */ -public class InteractiveQueryService { - - private static final Log LOG = LogFactory.getLog(InteractiveQueryService.class); - - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - private final KafkaStreamsBinderConfigurationProperties binderConfigurationProperties; - - private final KafkaStreamsVersionAgnosticTopologyInfoFacade topologyInfoFacade; - - private StoreQueryParametersCustomizer storeQueryParametersCustomizer; - - /** - * Constructor for InteractiveQueryService. - * @param kafkaStreamsRegistry holding {@link KafkaStreamsRegistry} - * @param binderConfigurationProperties kafka Streams binder configuration properties - */ - public InteractiveQueryService(KafkaStreamsRegistry kafkaStreamsRegistry, - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties) { - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - this.binderConfigurationProperties = binderConfigurationProperties; - this.topologyInfoFacade = new KafkaStreamsVersionAgnosticTopologyInfoFacade(); - } - - /** - * Retrieve and return a queryable store by name created in the application. - * @param storeName name of the queryable store - * @param storeType type of the queryable store - * @param generic queryable store - * @return queryable store. - */ - @SuppressWarnings("unchecked") - public T getQueryableStore(String storeName, QueryableStoreType storeType) { - - KafkaStreams contextSpecificKafkaStreams = getThreadContextSpecificKafkaStreams(); - - StoreQueryParameters storeQueryParams = StoreQueryParameters.fromNameAndType(storeName, storeType); - if (this.storeQueryParametersCustomizer != null) { - storeQueryParams = ((StoreQueryParametersCustomizer) this.storeQueryParametersCustomizer).customize(storeQueryParams); - } - - AtomicReference> storeQueryParametersAtomicReference = new AtomicReference<>(storeQueryParams); - - return getRetryTemplate().execute(context -> { - T store = null; - Throwable throwable = null; - if (contextSpecificKafkaStreams != null) { - try { - store = contextSpecificKafkaStreams.store(storeQueryParametersAtomicReference.get()); - } - catch (InvalidStateStoreException e) { - throwable = e; - } - } - if (store != null) { - return store; - } - if (contextSpecificKafkaStreams != null) { - LOG.warn("Store (" + storeName + ") could not be found in Streams context, falling back to all known Streams instances"); - } - - // Find all apps that know about the store - Map candidateStores = new HashMap<>(); - for (KafkaStreams kafkaStreamApp : kafkaStreamsRegistry.getKafkaStreams()) { - try { - candidateStores.put(kafkaStreamApp, kafkaStreamApp.store(storeQueryParametersAtomicReference.get())); - } - catch (Exception ex) { - throwable = ex; - } - } - - // Store exists in a single app - no further resolution required - if (candidateStores.size() == 1) { - return candidateStores.values().stream().findFirst().get(); - } - - // If the store is in multiple streams apps - discard any apps that do not actually have the store - if (candidateStores.size() > 1) { - - candidateStores = candidateStores.entrySet().stream() - .filter((e) -> this.topologyInfoFacade.streamsAppActuallyHasStore(e.getKey(), storeName)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - if (candidateStores.size() == 1) { - return candidateStores.values().stream().findFirst().get(); - } - - throwable = (candidateStores.size() == 0) ? - new UnknownStateStoreException("Store (" + storeName + ") not available to Streams instance") : - new InvalidStateStoreException("Store (" + storeName + ") available to more than one Streams instance"); - - } - throw new IllegalStateException("Error retrieving state store: " + storeName, throwable); - }); - } - - /** - * Retrieves the current {@link KafkaStreams} context if executing Thread is created by a Streams App (contains a matching application id in Thread's name). - * - * @return KafkaStreams instance associated with Thread - */ - private KafkaStreams getThreadContextSpecificKafkaStreams() { - return this.kafkaStreamsRegistry.getKafkaStreams().stream() - .filter(this::filterByThreadName).findAny().orElse(null); - } - - /** - * Checks if the supplied {@link KafkaStreams} instance belongs to the calling Thread by matching the Thread's name with the Streams Application Id. - * - * @param streams {@link KafkaStreams} instance to filter - * @return true if Streams Instance is associated with Thread - */ - private boolean filterByThreadName(KafkaStreams streams) { - String applicationId = kafkaStreamsRegistry.streamBuilderFactoryBean( - streams).getStreamsConfiguration() - .getProperty(StreamsConfig.APPLICATION_ID_CONFIG); - // TODO: is there some better way to find out if a Stream App created the Thread? - return Thread.currentThread().getName().contains(applicationId); - } - - /** - * Gets the current {@link HostInfo} that the calling kafka streams application is - * running on. - * - * Note that the end user applications must provide `application.server` as a - * configuration property when calling this method. If this is not available, then - * null is returned. - * @return the current {@link HostInfo} - */ - public HostInfo getCurrentHostInfo() { - Map configuration = this.binderConfigurationProperties - .getConfiguration(); - if (configuration.containsKey("application.server")) { - - String applicationServer = configuration.get("application.server"); - String[] splits = StringUtils.split(applicationServer, ":"); - - return new HostInfo(splits[0], Integer.parseInt(splits[1])); - } - return null; - } - - /** - * Gets the {@link HostInfo} where the provided store and key are hosted on. This may - * not be the current host that is running the application. Kafka Streams will look - * through all the consumer instances under the same application id and retrieves the - * proper host. - * - * Note that the end user applications must provide `application.server` as a - * configuration property for all the application instances when calling this method. - * If this is not available, then null maybe returned. - * @param generic type for key - * @param store store name - * @param key key to look for - * @param serializer {@link Serializer} for the key - * @return the {@link HostInfo} where the key for the provided store is hosted currently - */ - public HostInfo getHostInfo(String store, K key, Serializer serializer) { - final RetryTemplate retryTemplate = getRetryTemplate(); - - - return retryTemplate.execute(context -> { - Throwable throwable = null; - try { - final KeyQueryMetadata keyQueryMetadata = this.kafkaStreamsRegistry.getKafkaStreams() - .stream() - .map((k) -> Optional.ofNullable(k.queryMetadataForKey(store, key, serializer))) - .filter(Optional::isPresent).map(Optional::get).findFirst().orElse(null); - if (keyQueryMetadata != null) { - return keyQueryMetadata.activeHost(); - } - } - catch (Exception e) { - throwable = e; - } - throw new IllegalStateException( - "Error when retrieving state store.", throwable != null ? throwable : new Throwable("Kafka Streams is not ready.")); - }); - } - - private RetryTemplate getRetryTemplate() { - RetryTemplate retryTemplate = new RetryTemplate(); - - KafkaStreamsBinderConfigurationProperties.StateStoreRetry stateStoreRetry = this.binderConfigurationProperties.getStateStoreRetry(); - RetryPolicy retryPolicy = new SimpleRetryPolicy(stateStoreRetry.getMaxAttempts()); - FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); - backOffPolicy.setBackOffPeriod(stateStoreRetry.getBackoffPeriod()); - - retryTemplate.setBackOffPolicy(backOffPolicy); - retryTemplate.setRetryPolicy(retryPolicy); - - return retryTemplate; - } - - /** - * Retrieves and returns the {@link KeyQueryMetadata} associated with the given combination of - * key and state store. If none found, it will return null. - * - * @param generic type for key - * @param store store name - * @param key key to look for - * @param serializer {@link Serializer} for the key - * @return the {@link KeyQueryMetadata} if available, null otherwise. - */ - public KeyQueryMetadata getKeyQueryMetadata(String store, K key, Serializer serializer) { - return this.kafkaStreamsRegistry.getKafkaStreams() - .stream() - .map((k) -> Optional.ofNullable(k.queryMetadataForKey(store, key, serializer))) - .filter(Optional::isPresent).map(Optional::get).findFirst().orElse(null); - } - - /** - * Retrieves and returns the {@link KafkaStreams} object that is associated with the given combination of - * key and state store. If none found, it will return null. - * - * @param generic type for key - * @param store store name - * @param key key to look for - * @param serializer {@link Serializer} for the key - * @return {@link KafkaStreams} object associated with this combination of store and key - */ - public KafkaStreams getKafkaStreams(String store, K key, Serializer serializer) { - final AtomicReference kafkaStreamsAtomicReference = new AtomicReference<>(); - this.kafkaStreamsRegistry.getKafkaStreams() - .forEach(k -> { - final KeyQueryMetadata keyQueryMetadata = k.queryMetadataForKey(store, key, serializer); - if (keyQueryMetadata != null) { - kafkaStreamsAtomicReference.set(k); - } - }); - return kafkaStreamsAtomicReference.get(); - } - - /** - * Gets the list of {@link HostInfo} where the provided store is hosted on. - * It also can include current host info. - * Kafka Streams will look through all the consumer instances under the same application id - * and retrieves all hosts info. - * - * Note that the end-user applications must provide `application.server` as a configuration property - * for all the application instances when calling this method. If this is not available, then an empty list will be returned. - * - * @param store store name - * @return the list of {@link HostInfo} where provided store is hosted on - */ - public List getAllHostsInfo(String store) { - return kafkaStreamsRegistry.getKafkaStreams() - .stream() - .flatMap(k -> k.allMetadataForStore(store).stream()) - .filter(Objects::nonNull) - .map(StreamsMetadata::hostInfo) - .collect(Collectors.toList()); - } - - /** - * @param storeQueryParametersCustomizer to customize - * @since 4.0.1 - */ - public void setStoreQueryParametersCustomizer(StoreQueryParametersCustomizer storeQueryParametersCustomizer) { - this.storeQueryParametersCustomizer = storeQueryParametersCustomizer; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBinder.java deleted file mode 100644 index e36221964..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBinder.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams; - -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Produced; -import org.apache.kafka.streams.processor.StreamPartitioner; - -import org.springframework.aop.framework.Advised; -import org.springframework.cloud.stream.binder.AbstractBinder; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.DefaultBinding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsProducerProperties; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.StringUtils; - -/** - * {@link org.springframework.cloud.stream.binder.Binder} implementation for - * {@link KStream}. This implemenation extends from the {@link AbstractBinder} directly. - *

- * Provides both producer and consumer bindings for the bound KStream. - * - * @author Marius Bogoevici - * @author Soby Chacko - */ -class KStreamBinder extends - // @checkstyle:off - AbstractBinder, ExtendedConsumerProperties, ExtendedProducerProperties> - implements - ExtendedPropertiesBinder, KafkaStreamsConsumerProperties, KafkaStreamsProducerProperties> { - - // @checkstyle:on - - private static final Log LOG = LogFactory.getLog(KStreamBinder.class); - - private final KafkaTopicProvisioner kafkaTopicProvisioner; - - // @checkstyle:off - private KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties = new KafkaStreamsExtendedBindingProperties(); - - // @checkstyle:on - - private final KafkaStreamsBinderConfigurationProperties binderConfigurationProperties; - - private final KafkaStreamsMessageConversionDelegate kafkaStreamsMessageConversionDelegate; - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - private final KeyValueSerdeResolver keyValueSerdeResolver; - - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - KStreamBinder(KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsMessageConversionDelegate kafkaStreamsMessageConversionDelegate, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue, - KeyValueSerdeResolver keyValueSerdeResolver, KafkaStreamsRegistry kafkaStreamsRegistry) { - this.binderConfigurationProperties = binderConfigurationProperties; - this.kafkaTopicProvisioner = kafkaTopicProvisioner; - this.kafkaStreamsMessageConversionDelegate = kafkaStreamsMessageConversionDelegate; - this.kafkaStreamsBindingInformationCatalogue = KafkaStreamsBindingInformationCatalogue; - this.keyValueSerdeResolver = keyValueSerdeResolver; - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - } - - @Override - protected Binding> doBindConsumer(String name, String group, - KStream inputTarget, - ExtendedConsumerProperties properties) { - - KStream delegate = ((KStreamBoundElementFactory.KStreamWrapperHandler) - ((Advised) inputTarget).getAdvisors()[0].getAdvice()).getDelegate(); - - this.kafkaStreamsBindingInformationCatalogue.registerConsumerProperties(delegate, properties.getExtension()); - - if (!StringUtils.hasText(group)) { - group = properties.getExtension().getApplicationId(); - } - - final RetryTemplate retryTemplate = buildRetryTemplate(properties); - - final String bindingName = this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(inputTarget); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeanPerBinding().get(bindingName); - - KafkaStreamsBinderUtils.prepareConsumerBinding(name, group, - getApplicationContext(), this.kafkaTopicProvisioner, - this.binderConfigurationProperties, properties, retryTemplate, getBeanFactory(), - this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(inputTarget), - this.kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - - - return new DefaultBinding>(bindingName, group, - inputTarget, streamsBuilderFactoryBean) { - - @Override - public boolean isInput() { - return true; - } - - @Override - public synchronized void start() { - if (!streamsBuilderFactoryBean.isRunning()) { - super.start(); - KStreamBinder.this.kafkaStreamsRegistry.registerKafkaStreams(streamsBuilderFactoryBean); - //If we cached the previous KafkaStreams object (from a binding stop on the actuator), remove it. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - final String applicationId = (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG); - if (kafkaStreamsBindingInformationCatalogue.getStoppedKafkaStreams().containsKey(applicationId)) { - kafkaStreamsBindingInformationCatalogue.removePreviousKafkaStreamsForApplicationId(applicationId); - } - } - } - - @Override - public synchronized void stop() { - if (streamsBuilderFactoryBean.isRunning()) { - final KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - super.stop(); - KStreamBinder.this.kafkaStreamsRegistry.unregisterKafkaStreams(kafkaStreams); - KafkaStreamsBinderUtils.closeDlqProducerFactories(kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - //Caching the stopped KafkaStreams for health indicator purposes on the underlying processor. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - KStreamBinder.this.kafkaStreamsBindingInformationCatalogue.addPreviousKafkaStreamsForApplicationId( - (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG), kafkaStreams); - } - } - }; - } - - @Override - @SuppressWarnings("unchecked") - protected Binding> doBindProducer(String name, - KStream outboundBindTarget, - ExtendedProducerProperties properties) { - - ExtendedProducerProperties extendedProducerProperties = - (ExtendedProducerProperties) properties; - - this.kafkaTopicProvisioner.provisionProducerDestination(name, extendedProducerProperties); - Serde keySerde = this.keyValueSerdeResolver - .getOuboundKeySerde(properties.getExtension(), kafkaStreamsBindingInformationCatalogue.getOutboundKStreamResolvable(outboundBindTarget)); - LOG.info("Key Serde used for (outbound) " + name + ": " + keySerde.getClass().getName()); - - Serde valueSerde; - if (properties.isUseNativeEncoding()) { - valueSerde = this.keyValueSerdeResolver.getOutboundValueSerde(properties, - properties.getExtension(), kafkaStreamsBindingInformationCatalogue.getOutboundKStreamResolvable(outboundBindTarget)); - } - else { - valueSerde = Serdes.ByteArray(); - } - LOG.info("Value Serde used for (outbound) " + name + ": " + valueSerde.getClass().getName()); - - to(properties.isUseNativeEncoding(), name, outboundBindTarget, - (Serde) keySerde, (Serde) valueSerde, properties.getExtension()); - - final String bindingName = this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(outboundBindTarget); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeanPerBinding().get(bindingName); - - // We need the application id to pass to DefaultBinding so that it won't be interpreted as an anonymous group. - // In case, if we can't find application.id (which is unlikely), we just default to bindingName. - // This will only be used for lifecycle management through actuator endpoints. - final Properties streamsConfiguration = streamsBuilderFactoryBean.getStreamsConfiguration(); - final String applicationId = streamsConfiguration != null ? (String) streamsConfiguration.get("application.id") : bindingName; - - return new DefaultBinding>(bindingName, - applicationId, outboundBindTarget, streamsBuilderFactoryBean) { - - @Override - public boolean isInput() { - return false; - } - - @Override - public synchronized void start() { - if (!streamsBuilderFactoryBean.isRunning()) { - super.start(); - KStreamBinder.this.kafkaStreamsRegistry.registerKafkaStreams(streamsBuilderFactoryBean); - //If we cached the previous KafkaStreams object (from a binding stop on the actuator), remove it. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - final String applicationId = (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG); - if (kafkaStreamsBindingInformationCatalogue.getStoppedKafkaStreams().containsKey(applicationId)) { - kafkaStreamsBindingInformationCatalogue.removePreviousKafkaStreamsForApplicationId(applicationId); - } - } - } - - @Override - public synchronized void stop() { - if (streamsBuilderFactoryBean.isRunning()) { - final KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - super.stop(); - KStreamBinder.this.kafkaStreamsRegistry.unregisterKafkaStreams(kafkaStreams); - KafkaStreamsBinderUtils.closeDlqProducerFactories(kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - //Caching the stopped KafkaStreams for health indicator purposes on the underlying processor - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - KStreamBinder.this.kafkaStreamsBindingInformationCatalogue.addPreviousKafkaStreamsForApplicationId( - (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG), kafkaStreams); - } - } - }; - } - - @SuppressWarnings("unchecked") - private void to(boolean isNativeEncoding, String name, - KStream outboundBindTarget, Serde keySerde, - Serde valueSerde, KafkaStreamsProducerProperties properties) { - final Produced produced = Produced.with(keySerde, valueSerde); - if (StringUtils.hasText(properties.getProducedAs())) { - produced.withName(properties.getProducedAs()); - } - StreamPartitioner streamPartitioner = null; - if (StringUtils.hasText(properties.getStreamPartitionerBeanName())) { - streamPartitioner = getApplicationContext().getBean(properties.getStreamPartitionerBeanName(), - StreamPartitioner.class); - } - if (streamPartitioner != null) { - produced.withStreamPartitioner(streamPartitioner); - } - if (!isNativeEncoding) { - LOG.info("Native encoding is disabled for " + name - + ". Outbound message conversion done by Spring Cloud Stream."); - outboundBindTarget.filter((k, v) -> v == null) - .to(name, produced); - this.kafkaStreamsMessageConversionDelegate - .serializeOnOutbound(outboundBindTarget) - .to(name, produced); - } - else { - LOG.info("Native encoding is enabled for " + name - + ". Outbound serialization done at the broker."); - outboundBindTarget.to(name, produced); - } - } - - @Override - public KafkaStreamsConsumerProperties getExtendedConsumerProperties( - String channelName) { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedConsumerProperties(channelName); - } - - @Override - public KafkaStreamsProducerProperties getExtendedProducerProperties( - String channelName) { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedProducerProperties(channelName); - } - - public void setKafkaStreamsExtendedBindingProperties( - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties) { - this.kafkaStreamsExtendedBindingProperties = kafkaStreamsExtendedBindingProperties; - } - - @Override - public String getDefaultsPrefix() { - return this.kafkaStreamsExtendedBindingProperties.getDefaultsPrefix(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedPropertiesEntryClass(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBinderConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBinderConfiguration.java deleted file mode 100644 index 93d4e269f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBinderConfiguration.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * Configuration for KStream binder. - * - * @author Marius Bogoevici - * @author Gary Russell - * @author Soby Chacko - * @author Chris Bono - */ -@Configuration -@Import({ MultiBinderPropertiesConfiguration.class, - KafkaStreamsBinderHealthIndicatorConfiguration.class, - KafkaStreamsJaasConfiguration.class}) -@EnableConfigurationProperties(KafkaProperties.class) -public class KStreamBinderConfiguration { - - @Bean - public KafkaTopicProvisioner provisioningProvider( - KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties, - KafkaProperties kafkaProperties, ObjectProvider adminClientConfigCustomizer) { - return new KafkaTopicProvisioner(kafkaStreamsBinderConfigurationProperties, - kafkaProperties, adminClientConfigCustomizer.getIfUnique()); - } - - @Bean - public KStreamBinder kStreamBinder( - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsMessageConversionDelegate KafkaStreamsMessageConversionDelegate, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue, - KeyValueSerdeResolver keyValueSerdeResolver, - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties, KafkaStreamsRegistry kafkaStreamsRegistry) { - KStreamBinder kStreamBinder = new KStreamBinder(binderConfigurationProperties, - kafkaTopicProvisioner, KafkaStreamsMessageConversionDelegate, - KafkaStreamsBindingInformationCatalogue, keyValueSerdeResolver, kafkaStreamsRegistry); - kStreamBinder.setKafkaStreamsExtendedBindingProperties( - kafkaStreamsExtendedBindingProperties); - return kStreamBinder; - } - - @Bean - @ConditionalOnBean(name = "outerContext") - public static BeanFactoryPostProcessor outerContextBeanFactoryPostProcessor() { - return beanFactory -> { - - // It is safe to call getBean("outerContext") here, because this bean is - // registered as first - // and as independent from the parent context. - ApplicationContext outerContext = (ApplicationContext) beanFactory - .getBean("outerContext"); - beanFactory.registerSingleton( - KafkaStreamsMessageConversionDelegate.class.getSimpleName(), - outerContext.getBean(KafkaStreamsMessageConversionDelegate.class)); - beanFactory.registerSingleton( - KafkaStreamsBindingInformationCatalogue.class.getSimpleName(), - outerContext.getBean(KafkaStreamsBindingInformationCatalogue.class)); - beanFactory.registerSingleton(KeyValueSerdeResolver.class.getSimpleName(), - outerContext.getBean(KeyValueSerdeResolver.class)); - beanFactory.registerSingleton( - KafkaStreamsExtendedBindingProperties.class.getSimpleName(), - outerContext.getBean(KafkaStreamsExtendedBindingProperties.class)); - beanFactory.registerSingleton( - KafkaStreamsRegistry.class.getSimpleName(), - outerContext.getBean(KafkaStreamsRegistry.class)); - }; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBoundElementFactory.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBoundElementFactory.java deleted file mode 100644 index d484a5ba7..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KStreamBoundElementFactory.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.kafka.streams.kstream.KStream; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binding.AbstractBindingTargetFactory; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.util.Assert; - -/** - * {@link org.springframework.cloud.stream.binding.BindingTargetFactory} - * for{@link KStream}. - * - * The implementation creates proxies for both input and output binding. The actual target - * will be created downstream through further binding process. - * - * @author Marius Bogoevici - * @author Soby Chacko - */ -class KStreamBoundElementFactory extends AbstractBindingTargetFactory { - - private final BindingServiceProperties bindingServiceProperties; - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - private final EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler; - - KStreamBoundElementFactory(BindingServiceProperties bindingServiceProperties, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue, - EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler) { - super(KStream.class); - this.bindingServiceProperties = bindingServiceProperties; - this.kafkaStreamsBindingInformationCatalogue = KafkaStreamsBindingInformationCatalogue; - this.encodingDecodingBindAdviceHandler = encodingDecodingBindAdviceHandler; - } - - @Override - public KStream createInput(String name) { - BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(name); - ConsumerProperties consumerProperties = bindingProperties.getConsumer(); - if (consumerProperties == null) { - consumerProperties = this.bindingServiceProperties.getConsumerProperties(name); - consumerProperties.setUseNativeDecoding(true); - } - else { - if (!encodingDecodingBindAdviceHandler.isDecodingSettingProvided()) { - consumerProperties.setUseNativeDecoding(true); - } - } - // Always set multiplex to true in the kafka streams binder - consumerProperties.setMultiplex(true); - return createProxyForKStream(name); - } - - @Override - @SuppressWarnings("unchecked") - public KStream createOutput(final String name) { - - BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(name); - ProducerProperties producerProperties = bindingProperties.getProducer(); - if (producerProperties == null) { - producerProperties = this.bindingServiceProperties.getProducerProperties(name); - producerProperties.setUseNativeEncoding(true); - } - else { - if (!encodingDecodingBindAdviceHandler.isEncodingSettingProvided()) { - producerProperties.setUseNativeEncoding(true); - } - } - return createProxyForKStream(name); - } - - private KStream createProxyForKStream(String name) { - KStreamWrapperHandler wrapper = new KStreamWrapperHandler(); - ProxyFactory proxyFactory = new ProxyFactory(KStreamWrapper.class, KStream.class); - proxyFactory.addAdvice(wrapper); - - KStream proxy = (KStream) proxyFactory.getProxy(); - - // Add the binding properties to the catalogue for later retrieval during further - // binding steps downstream. - BindingProperties bindingProperties = this.bindingServiceProperties - .getBindingProperties(name); - this.kafkaStreamsBindingInformationCatalogue.registerBindingProperties(proxy, - bindingProperties); - this.kafkaStreamsBindingInformationCatalogue.addBindingNamePerTarget(proxy, name); - return proxy; - } - - /** - * Wrapper object for KStream proxy. - */ - public interface KStreamWrapper { - - void wrap(KStream delegate); - - } - - static class KStreamWrapperHandler - implements KStreamWrapper, MethodInterceptor { - - private KStream delegate; - - public void wrap(KStream delegate) { - Assert.notNull(delegate, "delegate cannot be null"); - if (this.delegate == null) { - this.delegate = delegate; - } - } - - @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { - if (methodInvocation.getMethod().getDeclaringClass().equals(KStream.class)) { - Assert.notNull(this.delegate, - "Trying to prepareConsumerBinding " + methodInvocation.getMethod() - + " but no delegate has been set."); - return methodInvocation.getMethod().invoke(this.delegate, - methodInvocation.getArguments()); - } - else if (methodInvocation.getMethod().getDeclaringClass() - .equals(KStreamWrapper.class)) { - return methodInvocation.getMethod().invoke(this, - methodInvocation.getArguments()); - } - else { - throw new IllegalStateException( - "Only KStream method invocations are permitted"); - } - } - - KStream getDelegate() { - return delegate; - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBinder.java deleted file mode 100644 index a5dcd9c09..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBinder.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.cloud.stream.binder.AbstractBinder; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.DefaultBinding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsProducerProperties; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.StringUtils; - -/** - * {@link org.springframework.cloud.stream.binder.Binder} implementation for - * {@link KTable}. This implemenation extends from the {@link AbstractBinder} directly. - * - * Provides only consumer binding for the bound KTable as output bindings are not allowed - * on it. - * - * @author Soby Chacko - */ -class KTableBinder extends - AbstractBinder, ExtendedConsumerProperties, ExtendedProducerProperties> - implements - ExtendedPropertiesBinder, KafkaStreamsConsumerProperties, KafkaStreamsProducerProperties> { - - private final KafkaStreamsBinderConfigurationProperties binderConfigurationProperties; - - private final KafkaTopicProvisioner kafkaTopicProvisioner; - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - // @checkstyle:off - private KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties = new KafkaStreamsExtendedBindingProperties(); - - // @checkstyle:on - - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - KTableBinder(KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue, KafkaStreamsRegistry kafkaStreamsRegistry) { - this.binderConfigurationProperties = binderConfigurationProperties; - this.kafkaTopicProvisioner = kafkaTopicProvisioner; - this.kafkaStreamsBindingInformationCatalogue = KafkaStreamsBindingInformationCatalogue; - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - } - - @Override - @SuppressWarnings("unchecked") - protected Binding> doBindConsumer(String name, String group, - KTable inputTarget, - // @checkstyle:off - ExtendedConsumerProperties properties) { - // @checkstyle:on - if (!StringUtils.hasText(group)) { - group = properties.getExtension().getApplicationId(); - } - - final RetryTemplate retryTemplate = buildRetryTemplate(properties); - - final String bindingName = this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(inputTarget); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeanPerBinding().get(bindingName); - - KafkaStreamsBinderUtils.prepareConsumerBinding(name, group, - getApplicationContext(), this.kafkaTopicProvisioner, - this.binderConfigurationProperties, properties, retryTemplate, getBeanFactory(), - this.kafkaStreamsBindingInformationCatalogue.bindingNamePerTarget(inputTarget), - this.kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - - return new DefaultBinding>(bindingName, group, inputTarget, streamsBuilderFactoryBean) { - - @Override - public boolean isInput() { - return true; - } - - @Override - public synchronized void start() { - if (!streamsBuilderFactoryBean.isRunning()) { - super.start(); - KTableBinder.this.kafkaStreamsRegistry.registerKafkaStreams(streamsBuilderFactoryBean); - //If we cached the previous KafkaStreams object (from a binding stop on the actuator), remove it. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - final String applicationId = (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG); - if (kafkaStreamsBindingInformationCatalogue.getStoppedKafkaStreams().containsKey(applicationId)) { - kafkaStreamsBindingInformationCatalogue.removePreviousKafkaStreamsForApplicationId(applicationId); - } - } - } - - @Override - public synchronized void stop() { - if (streamsBuilderFactoryBean.isRunning()) { - final KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - super.stop(); - KTableBinder.this.kafkaStreamsRegistry.unregisterKafkaStreams(kafkaStreams); - KafkaStreamsBinderUtils.closeDlqProducerFactories(kafkaStreamsBindingInformationCatalogue, streamsBuilderFactoryBean); - //Caching the stopped KafkaStreams for health indicator purposes on the underlying processor. - //See this issue for more details: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - KTableBinder.this.kafkaStreamsBindingInformationCatalogue.addPreviousKafkaStreamsForApplicationId( - (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG), kafkaStreams); - } - } - }; - } - - @Override - protected Binding> doBindProducer(String name, - KTable outboundBindTarget, - ExtendedProducerProperties properties) { - throw new UnsupportedOperationException( - "No producer level binding is allowed for KTable"); - } - - @Override - public KafkaStreamsConsumerProperties getExtendedConsumerProperties( - String channelName) { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedConsumerProperties(channelName); - } - - @Override - public KafkaStreamsProducerProperties getExtendedProducerProperties( - String channelName) { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedProducerProperties(channelName); - } - - @Override - public String getDefaultsPrefix() { - return this.kafkaStreamsExtendedBindingProperties.getDefaultsPrefix(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return this.kafkaStreamsExtendedBindingProperties - .getExtendedPropertiesEntryClass(); - } - - public void setKafkaStreamsExtendedBindingProperties( - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties) { - this.kafkaStreamsExtendedBindingProperties = kafkaStreamsExtendedBindingProperties; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBinderConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBinderConfiguration.java deleted file mode 100644 index 48f8543f9..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBinderConfiguration.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams; - -import java.util.Map; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * Configuration for KTable binder. - * - * @author Soby Chacko - * @author Chris Bono - */ -@SuppressWarnings("ALL") -@Configuration -@Import({ MultiBinderPropertiesConfiguration.class, - KafkaStreamsBinderHealthIndicatorConfiguration.class, - KafkaStreamsJaasConfiguration.class}) -@EnableConfigurationProperties(KafkaProperties.class) -public class KTableBinderConfiguration { - - @Bean - public KafkaTopicProvisioner provisioningProvider( - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaProperties kafkaProperties, ObjectProvider adminClientConfigCustomizer) { - return new KafkaTopicProvisioner(binderConfigurationProperties, kafkaProperties, adminClientConfigCustomizer.getIfUnique()); - } - - @Bean - public KTableBinder kTableBinder( - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - @Qualifier("streamConfigGlobalProperties") Map streamConfigGlobalProperties, - KafkaStreamsRegistry kafkaStreamsRegistry) { - KTableBinder kTableBinder = new KTableBinder(binderConfigurationProperties, - kafkaTopicProvisioner, kafkaStreamsBindingInformationCatalogue, kafkaStreamsRegistry); - kTableBinder.setKafkaStreamsExtendedBindingProperties(kafkaStreamsExtendedBindingProperties); - return kTableBinder; - } - - @Bean - @ConditionalOnBean(name = "outerContext") - public static BeanFactoryPostProcessor outerContextBeanFactoryPostProcessor() { - return beanFactory -> { - - // It is safe to call getBean("outerContext") here, because this bean is - // registered as first - // and as independent from the parent context. - ApplicationContext outerContext = (ApplicationContext) beanFactory - .getBean("outerContext"); - beanFactory.registerSingleton( - KafkaStreamsExtendedBindingProperties.class.getSimpleName(), - outerContext.getBean(KafkaStreamsExtendedBindingProperties.class)); - beanFactory.registerSingleton( - KafkaStreamsBindingInformationCatalogue.class.getSimpleName(), - outerContext.getBean(KafkaStreamsBindingInformationCatalogue.class)); - beanFactory.registerSingleton( - KafkaStreamsRegistry.class.getSimpleName(), - outerContext.getBean(KafkaStreamsRegistry.class)); - }; - } - - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBoundElementFactory.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBoundElementFactory.java deleted file mode 100644 index 2dc7a90a7..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KTableBoundElementFactory.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binding.AbstractBindingTargetFactory; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.util.Assert; - -/** - * {@link org.springframework.cloud.stream.binding.BindingTargetFactory} for - * {@link KTable} - * - * Input bindings are only created as output bindings on KTable are not allowed. - * - * @author Soby Chacko - */ -class KTableBoundElementFactory extends AbstractBindingTargetFactory { - - private final BindingServiceProperties bindingServiceProperties; - private final EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler; - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - KTableBoundElementFactory(BindingServiceProperties bindingServiceProperties, - EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue) { - super(KTable.class); - this.bindingServiceProperties = bindingServiceProperties; - this.encodingDecodingBindAdviceHandler = encodingDecodingBindAdviceHandler; - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - } - - @Override - public KTable createInput(String name) { - BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(name); - ConsumerProperties consumerProperties = bindingProperties.getConsumer(); - if (consumerProperties == null) { - consumerProperties = this.bindingServiceProperties.getConsumerProperties(name); - consumerProperties.setUseNativeDecoding(true); - } - else { - if (!encodingDecodingBindAdviceHandler.isDecodingSettingProvided()) { - consumerProperties.setUseNativeDecoding(true); - } - } - // Always set multiplex to true in the kafka streams binder - consumerProperties.setMultiplex(true); - - KTableBoundElementFactory.KTableWrapperHandler wrapper = new KTableBoundElementFactory.KTableWrapperHandler(); - ProxyFactory proxyFactory = new ProxyFactory( - KTableBoundElementFactory.KTableWrapper.class, KTable.class); - proxyFactory.addAdvice(wrapper); - - final KTable proxy = (KTable) proxyFactory.getProxy(); - this.kafkaStreamsBindingInformationCatalogue.addBindingNamePerTarget(proxy, name); - return proxy; - } - - @Override - @SuppressWarnings("unchecked") - public KTable createOutput(final String name) { - throw new UnsupportedOperationException( - "Outbound operations are not allowed on target type KTable"); - } - - /** - * Wrapper for KTable proxy. - */ - public interface KTableWrapper { - - void wrap(KTable delegate); - - } - - private static class KTableWrapperHandler - implements KTableBoundElementFactory.KTableWrapper, MethodInterceptor { - - private KTable delegate; - - public void wrap(KTable delegate) { - Assert.notNull(delegate, "delegate cannot be null"); - if (this.delegate == null) { - this.delegate = delegate; - } - } - - @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { - if (methodInvocation.getMethod().getDeclaringClass().equals(KTable.class)) { - Assert.notNull(this.delegate, - "Trying to prepareConsumerBinding " + methodInvocation.getMethod() - + " but no delegate has been set."); - return methodInvocation.getMethod().invoke(this.delegate, - methodInvocation.getArguments()); - } - else if (methodInvocation.getMethod().getDeclaringClass() - .equals(KTableBoundElementFactory.KTableWrapper.class)) { - return methodInvocation.getMethod().invoke(this, - methodInvocation.getArguments()); - } - else { - throw new IllegalStateException( - "Only KTable method invocations are permitted"); - } - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderEnvironmentPostProcessor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderEnvironmentPostProcessor.java deleted file mode 100644 index de514f7f0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderEnvironmentPostProcessor.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.streams; - -import java.util.Collections; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; - -/** - * {@link EnvironmentPostProcessor} to ensure the {@link SendToDlqAndContinue sendToDlqAndContinue} BiFunction - * is excluded in core Spring Cloud Function by adding it to the ineligible function definitions. - * - * @author Soby Chacko - * @author Chris Bono - */ -public class KafkaStreamsBinderEnvironmentPostProcessor implements EnvironmentPostProcessor { - - /** - * The bean name of the SendToDlqAndContinue function - must remain in sync w/ - * {@link KafkaStreamsBinderSupportAutoConfiguration#sendToDlqAndContinue()}. - */ - private static final String SEND_TO_DLQ_AND_CONTINUE_BEAN_NAME = "sendToDlqAndContinue"; - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - String ineligibleDefinitionsPropertyKey = "spring.cloud.function.ineligible-definitions"; - String ineligibleDefinitions = SEND_TO_DLQ_AND_CONTINUE_BEAN_NAME; - if (environment.getProperty(ineligibleDefinitionsPropertyKey) != null) { - ineligibleDefinitions += ("," + environment.getProperty(ineligibleDefinitionsPropertyKey)); - } - environment.getPropertySources().addFirst(new MapPropertySource( - "kafkaStreamsBinderIneligibleDefinitions", - Collections.singletonMap(ineligibleDefinitionsPropertyKey, ineligibleDefinitions))); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderHealthIndicator.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderHealthIndicator.java deleted file mode 100644 index 93eb00cf1..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderHealthIndicator.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; - -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.ListTopicsResult; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.TaskMetadata; -import org.apache.kafka.streams.ThreadMetadata; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; - -/** - * Health indicator for Kafka Streams. - * - * @author Arnaud JardinΓ© - * @author Soby Chacko - */ -public class KafkaStreamsBinderHealthIndicator extends AbstractHealthIndicator implements DisposableBean { - - /** - * Static initialization for detecting whether the application is using Kafka client 2.5 vs lower versions. - */ - private static ClassLoader CLASS_LOADER = KafkaStreamsBinderHealthIndicator.class.getClassLoader(); - private static boolean isKafkaStreams25 = true; - private static Method methodForIsRunning; - - static { - try { - Class KAFKA_STREAMS_STATE_CLASS = CLASS_LOADER.loadClass("org.apache.kafka.streams.KafkaStreams$State"); - - Method[] declaredMethods = KAFKA_STREAMS_STATE_CLASS.getDeclaredMethods(); - for (Method m : declaredMethods) { - if (m.getName().equals("isRunning")) { - isKafkaStreams25 = false; - methodForIsRunning = m; - } - } - } - catch (ClassNotFoundException e) { - throw new IllegalStateException("KafkaStreams$State class not found", e); - } - } - - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - private final KafkaStreamsBinderConfigurationProperties configurationProperties; - - private final Map adminClientProperties; - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - private AdminClient adminClient; - - private final Lock lock = new ReentrantLock(); - - KafkaStreamsBinderHealthIndicator(KafkaStreamsRegistry kafkaStreamsRegistry, - KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties, - KafkaProperties kafkaProperties, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue) { - super("Kafka-streams health check failed"); - kafkaProperties.buildAdminProperties(); - this.configurationProperties = kafkaStreamsBinderConfigurationProperties; - this.adminClientProperties = kafkaProperties.buildAdminProperties(); - KafkaTopicProvisioner.normalalizeBootPropsWithBinder(this.adminClientProperties, kafkaProperties, - kafkaStreamsBinderConfigurationProperties); - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - try { - this.lock.lock(); - if (this.adminClient == null) { - this.adminClient = AdminClient.create(this.adminClientProperties); - } - - final ListTopicsResult listTopicsResult = this.adminClient.listTopics(); - listTopicsResult.listings().get(this.configurationProperties.getHealthTimeout(), TimeUnit.SECONDS); - - if (this.kafkaStreamsBindingInformationCatalogue.getStreamsBuilderFactoryBeans().isEmpty()) { - builder.withDetail("No Kafka Streams bindings have been established", "Kafka Streams binder did not detect any processors"); - builder.status(Status.UNKNOWN); - } - else { - boolean up = true; - final Set kafkaStreams = kafkaStreamsRegistry.getKafkaStreams(); - Set allKafkaStreams = new HashSet<>(kafkaStreams); - if (this.configurationProperties.isIncludeStoppedProcessorsForHealthCheck()) { - allKafkaStreams.addAll(kafkaStreamsBindingInformationCatalogue.getStoppedKafkaStreams().values()); - } - for (KafkaStreams kStream : allKafkaStreams) { - if (isKafkaStreams25) { - up &= kStream.state().isRunningOrRebalancing(); - } - else { - // if Kafka client version is lower than 2.5, then call the method reflectively. - final boolean isRunningInvokedResult = (boolean) methodForIsRunning.invoke(kStream.state()); - up &= isRunningInvokedResult; - } - builder.withDetails(buildDetails(kStream)); - } - builder.status(up ? Status.UP : Status.DOWN); - } - } - catch (Exception e) { - builder.withDetail("No topic information available", "Kafka broker is not reachable"); - builder.status(Status.DOWN); - builder.withException(e); - } - finally { - this.lock.unlock(); - } - } - - private Map buildDetails(KafkaStreams kafkaStreams) throws Exception { - final Map details = new HashMap<>(); - final Map perAppdIdDetails = new HashMap<>(); - - boolean isRunningResult; - if (isKafkaStreams25) { - isRunningResult = kafkaStreams.state().isRunningOrRebalancing(); - } - else { - // if Kafka client version is lower than 2.5, then call the method reflectively. - isRunningResult = (boolean) methodForIsRunning.invoke(kafkaStreams.state()); - } - - if (isRunningResult) { - final Set threadMetadata = kafkaStreams.metadataForLocalThreads(); - final Map threadDetails = new HashMap<>(); - for (ThreadMetadata metadata : threadMetadata) { - final Map threadDetail = new HashMap<>(); - threadDetail.put("threadName", metadata.threadName()); - threadDetail.put("threadState", metadata.threadState()); - threadDetail.put("adminClientId", metadata.adminClientId()); - threadDetail.put("consumerClientId", metadata.consumerClientId()); - threadDetail.put("restoreConsumerClientId", metadata.restoreConsumerClientId()); - threadDetail.put("producerClientIds", metadata.producerClientIds()); - threadDetail.put("activeTasks", taskDetails(metadata.activeTasks())); - threadDetail.put("standbyTasks", taskDetails(metadata.standbyTasks())); - threadDetails.put(metadata.threadName(), threadDetail); - } - perAppdIdDetails.put("threadDetails", threadDetails); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsRegistry.streamBuilderFactoryBean(kafkaStreams); - final String applicationId = (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG); - details.put(applicationId, perAppdIdDetails); - } - else { - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsRegistry.streamBuilderFactoryBean(kafkaStreams); - String applicationId = null; - if (streamsBuilderFactoryBean != null) { - applicationId = (String) streamsBuilderFactoryBean.getStreamsConfiguration().get(StreamsConfig.APPLICATION_ID_CONFIG); - } - else { - final Map stoppedKafkaStreamsPerBinding = kafkaStreamsBindingInformationCatalogue.getStoppedKafkaStreams(); - for (String appId : stoppedKafkaStreamsPerBinding.keySet()) { - if (stoppedKafkaStreamsPerBinding.get(appId).equals(kafkaStreams)) { - applicationId = appId; - } - } - } - details.put(applicationId, String.format("The processor with application.id %s is down. Current state: %s", applicationId, kafkaStreams.state())); - } - return details; - } - - private static Map taskDetails(Set taskMetadata) { - final Map details = new HashMap<>(); - for (TaskMetadata metadata : taskMetadata) { - details.put("taskId", metadata.taskId()); - if (details.containsKey("partitions")) { - @SuppressWarnings("unchecked") - List partitionsInfo = (List) details.get("partitions"); - partitionsInfo.addAll(addPartitionsInfo(metadata)); - } - else { - details.put("partitions", - addPartitionsInfo(metadata)); - } - } - return details; - } - - private static List addPartitionsInfo(TaskMetadata metadata) { - return metadata.topicPartitions().stream().map( - p -> "partition=" + p.partition() + ", topic=" + p.topic()) - .collect(Collectors.toList()); - } - - @Override - public void destroy() throws Exception { - if (adminClient != null) { - adminClient.close(Duration.ofSeconds(0)); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderHealthIndicatorConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderHealthIndicatorConfiguration.java deleted file mode 100644 index 20fc76085..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderHealthIndicatorConfiguration.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration class for Kafka-streams binder health indicator beans. - * - * @author Arnaud JardinΓ© - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") -@ConditionalOnEnabledHealthIndicator("binders") -public class KafkaStreamsBinderHealthIndicatorConfiguration { - - @Bean - public KafkaStreamsBinderHealthIndicator kafkaStreamsBinderHealthIndicator( - ObjectProvider kafkaStreamsRegistry, - @Qualifier("binderConfigurationProperties")KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties, - KafkaProperties kafkaProperties, KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue) { - if (kafkaStreamsRegistry.getIfUnique() != null) { - return new KafkaStreamsBinderHealthIndicator(kafkaStreamsRegistry.getIfUnique(), kafkaStreamsBinderConfigurationProperties, - kafkaProperties, kafkaStreamsBindingInformationCatalogue); - } - return null; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderMetrics.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderMetrics.java deleted file mode 100644 index 8cdb0bd78..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderMetrics.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.function.ToDoubleFunction; - -import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.binder.MeterBinder; -import org.apache.kafka.common.Metric; -import org.apache.kafka.common.MetricName; -import org.apache.kafka.streams.KafkaStreams; - -import org.springframework.kafka.config.StreamsBuilderFactoryBean; - -/** - * Kafka Streams binder metrics implementation that exports the metrics available - * through {@link KafkaStreams#metrics()} into a micrometer {@link io.micrometer.core.instrument.MeterRegistry}. - * - * Boot 2.2 users need to rely on this class for the metrics instead of direct support from Micrometer. - * Micrometer added Kafka Streams metrics support in 1.4.0 which Boot 2.3 includes. - * Therefore, the users who are on Boot 2.2, need to rely on these metrics. - * For users who are on 2.3 Boot, this class won't be activated (See the configuration for the various - * conditionals used). - * - * For the most part, this class is a copy of the Micrometer Kafka Streams support that was added in version 1.4.0. - * We will keep this class, as long as we support Boot 2.2.x. - * - * @author Soby Chacko - * @since 3.0.0 - */ -public class KafkaStreamsBinderMetrics { - - static final String DEFAULT_VALUE = "unknown"; - - static final String CLIENT_ID_TAG_NAME = "client-id"; - - static final String METRIC_GROUP_APP_INFO = "app-info"; - - static final String VERSION_METRIC_NAME = "version"; - - static final String START_TIME_METRIC_NAME = "start-time-ms"; - - static final String KAFKA_VERSION_TAG_NAME = "kafka-version"; - - static final String METRIC_NAME_PREFIX = "kafka."; - - static final String METRIC_GROUP_METRICS_COUNT = "kafka-metrics-count"; - - private String kafkaVersion = DEFAULT_VALUE; - - private String clientId = DEFAULT_VALUE; - - private final MeterRegistry meterRegistry; - - private MeterBinder meterBinder; - - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - private volatile Set currentMeters = new HashSet<>(); - - public KafkaStreamsBinderMetrics(MeterRegistry meterRegistry) { - this.meterRegistry = meterRegistry; - } - - public void bindTo(Set streamsBuilderFactoryBeans) { - if (this.meterBinder == null) { - this.meterBinder = registry -> { - if (streamsBuilderFactoryBeans != null) { - for (StreamsBuilderFactoryBean streamsBuilderFactoryBean : streamsBuilderFactoryBeans) { - if (streamsBuilderFactoryBean.isRunning()) { - KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - final Map metrics = kafkaStreams.metrics(); - - prepareToBindMetrics(registry, metrics); - checkAndBindMetrics(registry, metrics); - } - } - } - }; - } - this.meterBinder.bindTo(this.meterRegistry); - } - - public void addMetrics(Set streamsBuilderFactoryBeans) { - synchronized (KafkaStreamsBinderMetrics.this) { - this.bindTo(streamsBuilderFactoryBeans); - } - } - - void prepareToBindMetrics(MeterRegistry registry, Map metrics) { - Metric startTime = null; - for (Map.Entry entry : metrics.entrySet()) { - MetricName name = entry.getKey(); - if (clientId.equals(DEFAULT_VALUE) && name.tags().get(CLIENT_ID_TAG_NAME) != null) { - clientId = name.tags().get(CLIENT_ID_TAG_NAME); - } - if (METRIC_GROUP_APP_INFO.equals(name.group())) { - if (VERSION_METRIC_NAME.equals(name.name())) { - kafkaVersion = (String) entry.getValue().metricValue(); - } - else if (START_TIME_METRIC_NAME.equals(name.name())) { - startTime = entry.getValue(); - } - } - } - if (startTime != null) { - bindMeter(registry, startTime, meterName(startTime), meterTags(startTime)); - } - } - - private void bindMeter(MeterRegistry registry, Metric metric, String name, Iterable tags) { - if (name.endsWith("total") || name.endsWith("count")) { - registerCounter(registry, metric, name, tags); - } - else { - registerGauge(registry, metric, name, tags); - } - } - - private void registerCounter(MeterRegistry registry, Metric metric, String name, Iterable tags) { - FunctionCounter.builder(name, metric, toMetricValue()) - .tags(tags) - .description(metric.metricName().description()) - .register(registry); - } - - private ToDoubleFunction toMetricValue() { - return metric -> ((Number) metric.metricValue()).doubleValue(); - } - - private void registerGauge(MeterRegistry registry, Metric metric, String name, Iterable tags) { - Gauge.builder(name, metric, toMetricValue()) - .tags(tags) - .description(metric.metricName().description()) - .register(registry); - } - - private List meterTags(Metric metric) { - return meterTags(metric, false); - } - - private String meterName(Metric metric) { - String name = METRIC_NAME_PREFIX + metric.metricName().group() + "." + metric.metricName().name(); - return name.replaceAll("-metrics", "").replaceAll("-", "."); - } - - private List meterTags(Metric metric, boolean includeCommonTags) { - List tags = new ArrayList<>(); - metric.metricName().tags().forEach((key, value) -> tags.add(Tag.of(key, value))); - tags.add(Tag.of(KAFKA_VERSION_TAG_NAME, kafkaVersion)); - return tags; - } - - private boolean differentClient(List tags) { - for (Tag tag : tags) { - if (tag.getKey().equals(CLIENT_ID_TAG_NAME)) { - if (!clientId.equals(tag.getValue())) { - return true; - } - } - } - return false; - } - - void checkAndBindMetrics(MeterRegistry registry, Map metrics) { - if (!currentMeters.equals(metrics.keySet())) { - currentMeters = new HashSet<>(metrics.keySet()); - metrics.forEach((name, metric) -> { - //Filter out non-numeric values - if (!(metric.metricValue() instanceof Number)) { - return; - } - - //Filter out metrics from groups that include metadata - if (METRIC_GROUP_APP_INFO.equals(name.group())) { - return; - } - if (METRIC_GROUP_METRICS_COUNT.equals(name.group())) { - return; - } - String meterName = meterName(metric); - List meterTagsWithCommonTags = meterTags(metric, true); - //Kafka has metrics with lower number of tags (e.g. with/without topic or partition tag) - //Remove meters with lower number of tags - boolean hasLessTags = false; - for (Meter other : registry.find(meterName).meters()) { - List tags = other.getId().getTags(); - // Only consider meters from the same client before filtering - if (differentClient(tags)) { - break; - } - if (tags.size() < meterTagsWithCommonTags.size()) { - registry.remove(other); - } - // Check if already exists - else if (tags.size() == meterTagsWithCommonTags.size()) { - if (tags.equals(meterTagsWithCommonTags)) { - return; - } - else { - break; - } - } - else { - hasLessTags = true; - } - } - if (hasLessTags) { - return; - } - bindMeter(registry, metric, meterName, meterTags(metric)); - }); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderSupportAutoConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderSupportAutoConfiguration.java deleted file mode 100644 index 430e2e7c9..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderSupportAutoConfiguration.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.stream.Collectors; - -import io.micrometer.core.instrument.MeterRegistry; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.errors.LogAndContinueExceptionHandler; -import org.apache.kafka.streams.errors.LogAndFailExceptionHandler; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.bind.BindResult; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; -import org.springframework.boot.context.properties.source.ConfigurationPropertySources; -import org.springframework.cloud.stream.binder.BinderConfiguration; -import org.springframework.cloud.stream.binder.kafka.streams.function.FunctionDetectorCondition; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.config.BinderProperties; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.support.utils.IntegrationUtils; -import org.springframework.kafka.config.KafkaStreamsConfiguration; -import org.springframework.kafka.config.StreamsBuilderFactoryBeanConfigurer; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.streams.KafkaStreamsMicrometerListener; -import org.springframework.kafka.streams.RecoveringDeserializationExceptionHandler; -import org.springframework.lang.Nullable; -import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * Kafka Streams binder configuration. - * - * @author Marius Bogoevici - * @author Soby Chacko - * @author Gary Russell - * @author Byungjun You - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties({ KafkaProperties.class, KafkaStreamsExtendedBindingProperties.class }) -@ConditionalOnBean(BindingService.class) -@AutoConfigureAfter(BindingServiceConfiguration.class) -public class KafkaStreamsBinderSupportAutoConfiguration { - - private static final String KSTREAM_BINDER_TYPE = "kstream"; - - private static final String KTABLE_BINDER_TYPE = "ktable"; - - private static final String GLOBALKTABLE_BINDER_TYPE = "globalktable"; - - private static final String CONSUMER_PROPERTIES_PREFIX = "consumer."; - private static final String PRODUCER_PROPERTIES_PREFIX = "producer."; - - @Bean - @ConfigurationProperties(prefix = "spring.cloud.stream.kafka.streams.binder") - public KafkaStreamsBinderConfigurationProperties binderConfigurationProperties( - KafkaProperties kafkaProperties, ConfigurableEnvironment environment, - BindingServiceProperties properties, ConfigurableApplicationContext context) throws Exception { - final Map binderConfigurations = getBinderConfigurations( - properties); - for (Map.Entry entry : binderConfigurations - .entrySet()) { - final BinderConfiguration binderConfiguration = entry.getValue(); - final String binderType = binderConfiguration.getBinderType(); - if (binderType != null && (binderType.equals(KSTREAM_BINDER_TYPE) - || binderType.equals(KTABLE_BINDER_TYPE) - || binderType.equals(GLOBALKTABLE_BINDER_TYPE))) { - Map binderProperties = new HashMap<>(); - this.flatten(null, binderConfiguration.getProperties(), binderProperties); - environment.getPropertySources().addFirst( - new MapPropertySource(entry.getKey() + "-kafkaStreamsBinderEnv", binderProperties)); - - Binder binder = new Binder(ConfigurationPropertySources.get(environment), - new PropertySourcesPlaceholdersResolver(environment), - IntegrationUtils.getConversionService(context.getBeanFactory()), null); - final Constructor kafkaStreamsBinderConfigurationPropertiesConstructor = - ReflectionUtils.accessibleConstructor(KafkaStreamsBinderConfigurationProperties.class, KafkaProperties.class); - final KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties = - BeanUtils.instantiateClass(kafkaStreamsBinderConfigurationPropertiesConstructor, kafkaProperties); - final BindResult bind = binder.bind("spring.cloud.stream.kafka.streams.binder", Bindable.ofInstance(kafkaStreamsBinderConfigurationProperties)); - context.getBeanFactory().registerSingleton( - entry.getKey() + "-KafkaStreamsBinderConfigurationProperties", - bind.get()); - } - } - return new KafkaStreamsBinderConfigurationProperties(kafkaProperties); - } - - // TODO: Lifted from core - good candidate for exposing as a utility method in core. - private static Map getBinderConfigurations( - BindingServiceProperties properties) { - - Map binderConfigurations = new HashMap<>(); - Map declaredBinders = properties.getBinders(); - - for (Map.Entry binderEntry : declaredBinders - .entrySet()) { - BinderProperties binderProperties = binderEntry.getValue(); - binderConfigurations.put(binderEntry.getKey(), - new BinderConfiguration(binderProperties.getType(), - binderProperties.getEnvironment(), - binderProperties.isInheritEnvironment(), - binderProperties.isDefaultCandidate())); - } - return binderConfigurations; - } - - // TODO: Lifted from core - good candidate for exposing as a utility method in core. - @SuppressWarnings("unchecked") - private void flatten(String propertyName, Object value, - Map flattenedProperties) { - if (value instanceof Map valueAsMap) { - valueAsMap.forEach((k, v) -> flatten( - (propertyName != null ? propertyName + "." : "") + k, v, - flattenedProperties)); - } - else { - flattenedProperties.put(propertyName, value.toString()); - } - } - - @Bean - public KafkaStreamsConfiguration kafkaStreamsConfiguration( - @Qualifier("binderConfigurationProperties") KafkaStreamsBinderConfigurationProperties properties, - Environment environment) { - KafkaProperties kafkaProperties = properties.getKafkaProperties(); - Map streamsProperties = kafkaProperties.buildStreamsProperties(); - if (kafkaProperties.getStreams().getApplicationId() == null) { - String applicationName = environment.getProperty("spring.application.name"); - if (applicationName != null) { - streamsProperties.put(StreamsConfig.APPLICATION_ID_CONFIG, - applicationName); - } - } - return new KafkaStreamsConfiguration(streamsProperties); - } - - @Bean("streamConfigGlobalProperties") - public Map streamConfigGlobalProperties( - @Qualifier("binderConfigurationProperties") KafkaStreamsBinderConfigurationProperties configProperties, - KafkaStreamsConfiguration kafkaStreamsConfiguration, ConfigurableEnvironment environment, - SendToDlqAndContinue sendToDlqAndContinue) { - - Properties properties = kafkaStreamsConfiguration.asProperties(); - - String kafkaConnectionString = configProperties.getKafkaConnectionString(); - - if (kafkaConnectionString != null && kafkaConnectionString.equals("localhost:9092")) { - //Making sure that the application indeed set a property. - String kafkaStreamsBinderBroker = environment.getProperty("spring.cloud.stream.kafka.streams.binder.brokers"); - - if (!StringUtils.hasText(kafkaStreamsBinderBroker)) { - //Kafka Streams binder specific property for brokers is not set by the application. - //See if there is one configured at the kafka binder level. - String kafkaBinderBroker = environment.getProperty("spring.cloud.stream.kafka.binder.brokers"); - if (StringUtils.hasText(kafkaBinderBroker)) { - kafkaConnectionString = kafkaBinderBroker; - configProperties.setBrokers(kafkaConnectionString); - } - } - } - - if (ObjectUtils.isEmpty(properties.get(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG))) { - properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, - kafkaConnectionString); - } - else { - Object bootstrapServerConfig = properties - .get(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG); - if (bootstrapServerConfig instanceof String bootStrapServers) { - if (bootStrapServers.equals("localhost:9092")) { - properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, - kafkaConnectionString); - } - } - else if (bootstrapServerConfig instanceof List bootStrapCollection) { - if (bootStrapCollection.size() == 1 && bootStrapCollection.get(0).equals("localhost:9092")) { - properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, - kafkaConnectionString); - } - } - } - - String binderProvidedApplicationId = configProperties.getApplicationId(); - if (StringUtils.hasText(binderProvidedApplicationId)) { - properties.put(StreamsConfig.APPLICATION_ID_CONFIG, - binderProvidedApplicationId); - } - - properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, - Serdes.ByteArraySerde.class.getName()); - properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, - Serdes.ByteArraySerde.class.getName()); - - if (configProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.logAndContinue) { - properties.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - LogAndContinueExceptionHandler.class); - } - else if (configProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.logAndFail) { - properties.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - LogAndFailExceptionHandler.class); - } - else if (configProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.sendToDlq) { - properties.put( - StreamsConfig.DEFAULT_DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG, - RecoveringDeserializationExceptionHandler.class); - properties.put(RecoveringDeserializationExceptionHandler.KSTREAM_DESERIALIZATION_RECOVERER, sendToDlqAndContinue); - } - - if (!ObjectUtils.isEmpty(configProperties.getConfiguration())) { - properties.putAll(configProperties.getConfiguration()); - } - - Map mergedConsumerConfig = new HashMap<>(configProperties.mergedConsumerConfiguration()); - //Adding consumer. prefix if they are missing (in order to differentiate them from other property categories such as stream, producer etc.) - addPrefix(properties, mergedConsumerConfig, CONSUMER_PROPERTIES_PREFIX); - - Map mergedProducerConfig = new HashMap<>(configProperties.mergedProducerConfiguration()); - //Adding producer. prefix if they are missing (in order to differentiate them from other property categories such as stream, consumer etc.) - addPrefix(properties, mergedProducerConfig, PRODUCER_PROPERTIES_PREFIX); - - if (!properties.containsKey(StreamsConfig.REPLICATION_FACTOR_CONFIG)) { - properties.put(StreamsConfig.REPLICATION_FACTOR_CONFIG, - (int) configProperties.getReplicationFactor()); - } - return properties.entrySet().stream().collect( - Collectors.toMap((e) -> String.valueOf(e.getKey()), Map.Entry::getValue)); - } - - private void addPrefix(Properties properties, Map mergedConsProdConfig, String prefix) { - Map mergedConfigs = new HashMap<>(); - for (String key : mergedConsProdConfig.keySet()) { - mergedConfigs.put(key.startsWith(prefix) ? key : prefix + key, mergedConsProdConfig.get(key)); - } - if (!ObjectUtils.isEmpty(mergedConfigs)) { - properties.putAll(mergedConfigs); - } - } - - @Bean - public KafkaStreamsMessageConversionDelegate messageConversionDelegate( - @Qualifier(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME) - CompositeMessageConverter compositeMessageConverter, - SendToDlqAndContinue sendToDlqAndContinue, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue, - @Qualifier("binderConfigurationProperties") KafkaStreamsBinderConfigurationProperties binderConfigurationProperties) { - return new KafkaStreamsMessageConversionDelegate(compositeMessageConverter, sendToDlqAndContinue, - KafkaStreamsBindingInformationCatalogue, binderConfigurationProperties); - } - - @Bean - public KStreamBoundElementFactory kStreamBoundElementFactory( - BindingServiceProperties bindingServiceProperties, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue, - EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler) { - return new KStreamBoundElementFactory(bindingServiceProperties, - KafkaStreamsBindingInformationCatalogue, encodingDecodingBindAdviceHandler); - } - - @Bean - public KTableBoundElementFactory kTableBoundElementFactory( - BindingServiceProperties bindingServiceProperties, EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue) { - return new KTableBoundElementFactory(bindingServiceProperties, encodingDecodingBindAdviceHandler, KafkaStreamsBindingInformationCatalogue); - } - - @Bean - public GlobalKTableBoundElementFactory globalKTableBoundElementFactory( - BindingServiceProperties properties, EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler, - KafkaStreamsBindingInformationCatalogue KafkaStreamsBindingInformationCatalogue) { - return new GlobalKTableBoundElementFactory(properties, encodingDecodingBindAdviceHandler, KafkaStreamsBindingInformationCatalogue); - } - - @Bean - public SendToDlqAndContinue sendToDlqAndContinue() { - return new SendToDlqAndContinue(); - } - - @Bean - public KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue() { - return new KafkaStreamsBindingInformationCatalogue(); - } - - @Bean - @SuppressWarnings("unchecked") - @ConditionalOnMissingBean - public KeyValueSerdeResolver keyValueSerdeResolver( - @Qualifier("streamConfigGlobalProperties") Object streamConfigGlobalProperties, - @Qualifier("binderConfigurationProperties")KafkaStreamsBinderConfigurationProperties properties) { - return new KeyValueSerdeResolver( - (Map) streamConfigGlobalProperties, properties); - } - - @Bean - public InteractiveQueryService interactiveQueryServices( - KafkaStreamsRegistry kafkaStreamsRegistry, - @Qualifier("binderConfigurationProperties")KafkaStreamsBinderConfigurationProperties properties, - ObjectProvider> storeQueryParametersCustomizerProvider) { - InteractiveQueryService interactiveQueryService = new InteractiveQueryService(kafkaStreamsRegistry, properties); - StoreQueryParametersCustomizer storeQueryParametersCustomizer = storeQueryParametersCustomizerProvider.getIfUnique(); - if (storeQueryParametersCustomizer != null) { - interactiveQueryService.setStoreQueryParametersCustomizer(storeQueryParametersCustomizer); - } - return interactiveQueryService; - } - - @Bean - public KafkaStreamsRegistry kafkaStreamsRegistry() { - return new KafkaStreamsRegistry(); - } - - @Bean - public StreamsBuilderFactoryManager streamsBuilderFactoryManager( - KafkaStreamsBindingInformationCatalogue catalogue, - KafkaStreamsRegistry kafkaStreamsRegistry, - @Nullable KafkaStreamsBinderMetrics kafkaStreamsBinderMetrics, - @Nullable KafkaStreamsMicrometerListener listener, KafkaProperties kafkaProperties) { - return new StreamsBuilderFactoryManager(catalogue, kafkaStreamsRegistry, kafkaStreamsBinderMetrics, listener, kafkaProperties); - } - - @Bean - @Conditional(FunctionDetectorCondition.class) - public KafkaStreamsFunctionProcessor kafkaStreamsFunctionProcessor(BindingServiceProperties bindingServiceProperties, - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties, - KeyValueSerdeResolver keyValueSerdeResolver, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - KafkaStreamsMessageConversionDelegate kafkaStreamsMessageConversionDelegate, - ObjectProvider cleanupConfig, - StreamFunctionProperties streamFunctionProperties, - @Qualifier("binderConfigurationProperties") KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties, - ObjectProvider customizerProvider, ConfigurableEnvironment environment) { - return new KafkaStreamsFunctionProcessor(bindingServiceProperties, kafkaStreamsExtendedBindingProperties, - keyValueSerdeResolver, kafkaStreamsBindingInformationCatalogue, kafkaStreamsMessageConversionDelegate, - cleanupConfig.getIfUnique(), streamFunctionProperties, kafkaStreamsBinderConfigurationProperties, - customizerProvider.getIfUnique(), environment); - } - - @Bean - public EncodingDecodingBindAdviceHandler encodingDecodingBindAdviceHandler() { - return new EncodingDecodingBindAdviceHandler(); - } - - @Bean - @ConditionalOnMissingBean - public DltPublishingContext dltSenderContext() { - return new DltPublishingContext(); - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(value = KafkaStreamsBinderMetrics.class, name = "outerContext") - @ConditionalOnClass(name = "io.micrometer.core.instrument.MeterRegistry") - protected class KafkaStreamsBinderMetricsConfiguration { - - @Bean - @ConditionalOnBean(MeterRegistry.class) - @ConditionalOnMissingBean(KafkaStreamsBinderMetrics.class) - @ConditionalOnMissingClass("org.springframework.kafka.core.MicrometerConsumerListener") - public KafkaStreamsBinderMetrics kafkaStreamsBinderMetrics(MeterRegistry meterRegistry) { - - return new KafkaStreamsBinderMetrics(meterRegistry); - } - - @ConditionalOnClass(name = "org.springframework.kafka.core.MicrometerConsumerListener") - @ConditionalOnBean(MeterRegistry.class) - protected class KafkaMicrometer { - - @Bean - @ConditionalOnMissingBean(name = "binderStreamsListener") - public KafkaStreamsMicrometerListener binderStreamsListener(MeterRegistry meterRegistry) { - return new KafkaStreamsMicrometerListener(meterRegistry); - } - } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(name = "outerContext") - @ConditionalOnMissingBean(KafkaStreamsBinderMetrics.class) - @ConditionalOnClass(name = "io.micrometer.core.instrument.MeterRegistry") - protected class KafkaStreamsBinderMetricsConfigurationWithMultiBinder { - - @Bean - @ConditionalOnMissingClass("org.springframework.kafka.core.MicrometerConsumerListener") - public KafkaStreamsBinderMetrics kafkaStreamsBinderMetrics(ConfigurableApplicationContext context) { - - MeterRegistry meterRegistry = context.getBean("outerContext", ApplicationContext.class) - .getBean(MeterRegistry.class); - return new KafkaStreamsBinderMetrics(meterRegistry); - } - - @ConditionalOnClass(name = "org.springframework.kafka.core.MicrometerConsumerListener") - @ConditionalOnBean(MeterRegistry.class) - protected class KafkaMicrometer { - - @Bean - @ConditionalOnMissingBean(name = "binderStreamsListener") - public KafkaStreamsMicrometerListener binderStreamsListener(MeterRegistry meterRegistry) { - return new KafkaStreamsMicrometerListener(meterRegistry); - } - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderUtils.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderUtils.java deleted file mode 100644 index cde79feaa..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderUtils.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiFunction; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.serialization.ByteArraySerializer; -import org.apache.kafka.streams.kstream.KStream; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.utils.DlqDestinationResolver; -import org.springframework.cloud.stream.binder.kafka.utils.DlqPartitionFunction; -import org.springframework.context.ApplicationContext; -import org.springframework.core.MethodParameter; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaOperations; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.listener.DeadLetterPublishingRecoverer; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * Common methods used by various Kafka Streams types across the binders. - * - * @author Soby Chacko - * @author Gary Russell - * @author James Forward - */ -public final class KafkaStreamsBinderUtils { - - private static final Log LOGGER = LogFactory.getLog(KafkaStreamsBinderUtils.class); - - private KafkaStreamsBinderUtils() { - - } - - /** - * Utility method to find the method targeted by the key. - * - * @param key name of the method - * @param methods collection of methods to search from - * @return found method as an {@link Optional} - */ - public static Optional findMethodWithName(String key, Method[] methods) { - return Arrays.stream(methods).filter(m -> m.getName().equals(key)).findFirst(); - } - - public static String[] deriveFunctionUnits(String definition) { - if (!StringUtils.hasText(definition)) { - return new String[]{}; - } - final String[] rawSplitDefinition = definition.split(";"); - return Arrays.stream(rawSplitDefinition).map(String::trim).toArray(String[]::new); - } - - static void prepareConsumerBinding(String name, String group, - ApplicationContext context, KafkaTopicProvisioner kafkaTopicProvisioner, - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties, - ExtendedConsumerProperties properties, - RetryTemplate retryTemplate, - ConfigurableListableBeanFactory beanFactory, String bindingName, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - StreamsBuilderFactoryBean streamsBuilderFactoryBean) { - - ExtendedConsumerProperties extendedConsumerProperties = - (ExtendedConsumerProperties) properties; - - if (binderConfigurationProperties - .getDeserializationExceptionHandler() == DeserializationExceptionHandler.sendToDlq) { - extendedConsumerProperties.getExtension().setEnableDlq(true); - } - // check for deserialization handler at the consumer binding, as that takes precedence. - final DeserializationExceptionHandler deserializationExceptionHandler = - properties.getExtension().getDeserializationExceptionHandler(); - if (deserializationExceptionHandler == DeserializationExceptionHandler.sendToDlq) { - extendedConsumerProperties.getExtension().setEnableDlq(true); - } - - String[] inputTopics = StringUtils.commaDelimitedListToStringArray(name); - for (String inputTopic : inputTopics) { - kafkaTopicProvisioner.provisionConsumerDestination(inputTopic, group, - extendedConsumerProperties); - } - - if (extendedConsumerProperties.getExtension().isEnableDlq()) { - - Map partitionFunctions = - context.getBeansOfType(DlqPartitionFunction.class, false, false); - boolean oneFunctionPresent = partitionFunctions.size() == 1; - Integer dlqPartitions = extendedConsumerProperties.getExtension().getDlqPartitions(); - DlqPartitionFunction partitionFunction = oneFunctionPresent - ? partitionFunctions.values().iterator().next() - : DlqPartitionFunction.determineFallbackFunction(dlqPartitions, LOGGER); - - ProducerFactory producerFactory = getProducerFactory( - new ExtendedProducerProperties<>( - extendedConsumerProperties.getExtension().getDlqProducerProperties()), - binderConfigurationProperties); - kafkaStreamsBindingInformationCatalogue.addDlqProducerFactory(streamsBuilderFactoryBean, producerFactory); - - KafkaOperations kafkaTemplate = new KafkaTemplate<>(producerFactory); - - Map dlqDestinationResolvers = - context.getBeansOfType(DlqDestinationResolver.class, false, false); - - BiFunction, Exception, TopicPartition> destinationResolver = - dlqDestinationResolvers.isEmpty() ? (cr, e) -> new TopicPartition(extendedConsumerProperties.getExtension().getDlqName(), - partitionFunction.apply(group, cr, e)) : - (cr, e) -> new TopicPartition(dlqDestinationResolvers.values().iterator().next().apply(cr, e), - partitionFunction.apply(group, cr, e)); - - DeadLetterPublishingRecoverer kafkaStreamsBinderDlqRecoverer = !dlqDestinationResolvers.isEmpty() || StringUtils - .hasText(extendedConsumerProperties.getExtension().getDlqName()) - ? new DeadLetterPublishingRecoverer(kafkaTemplate, destinationResolver) - : null; - for (String inputTopic : inputTopics) { - if (!StringUtils.hasText( - extendedConsumerProperties.getExtension().getDlqName()) && dlqDestinationResolvers.isEmpty()) { - destinationResolver = (cr, e) -> new TopicPartition("error." + inputTopic + "." + group, - partitionFunction.apply(group, cr, e)); - kafkaStreamsBinderDlqRecoverer = new DeadLetterPublishingRecoverer(kafkaTemplate, - destinationResolver); - } - SendToDlqAndContinue sendToDlqAndContinue = context - .getBean(SendToDlqAndContinue.class); - sendToDlqAndContinue.addKStreamDlqDispatch(inputTopic, - kafkaStreamsBinderDlqRecoverer); - } - } - - if (!StringUtils.hasText(properties.getRetryTemplateName())) { - @SuppressWarnings("unchecked") - BeanDefinition retryTemplateBeanDefinition = BeanDefinitionBuilder - .genericBeanDefinition( - (Class) retryTemplate.getClass(), - () -> retryTemplate) - .getRawBeanDefinition(); - ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(bindingName + "-RetryTemplate", retryTemplateBeanDefinition); - } - } - - private static DefaultKafkaProducerFactory getProducerFactory( - ExtendedProducerProperties producerProperties, - KafkaBinderConfigurationProperties configurationProperties) { - Map props = new HashMap<>(); - props.put(ProducerConfig.RETRIES_CONFIG, 0); - props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); - props.put(ProducerConfig.ACKS_CONFIG, configurationProperties.getRequiredAcks()); - Map mergedConfig = configurationProperties - .mergedProducerConfiguration(); - if (!ObjectUtils.isEmpty(mergedConfig)) { - props.putAll(mergedConfig); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG))) { - props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, - configurationProperties.getKafkaConnectionString()); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.BATCH_SIZE_CONFIG))) { - props.put(ProducerConfig.BATCH_SIZE_CONFIG, - String.valueOf(producerProperties.getExtension().getBufferSize())); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.LINGER_MS_CONFIG))) { - props.put(ProducerConfig.LINGER_MS_CONFIG, - String.valueOf(producerProperties.getExtension().getBatchTimeout())); - } - if (ObjectUtils.isEmpty(props.get(ProducerConfig.COMPRESSION_TYPE_CONFIG))) { - props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, - producerProperties.getExtension().getCompressionType().toString()); - } - Map configs = producerProperties.getExtension().getConfiguration(); - Assert.state(!configs.containsKey(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG), - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG + " cannot be overridden at the binding level; " - + "use multiple binders instead"); - if (!ObjectUtils.isEmpty(configs)) { - props.putAll(configs); - } - // Always send as byte[] on dlq (the same byte[] that the consumer received) - props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); - props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, - ByteArraySerializer.class); - - return new DefaultKafkaProducerFactory<>(props); - } - - - static boolean supportsKStream(MethodParameter methodParameter, Class targetBeanClass) { - return KStream.class.isAssignableFrom(targetBeanClass) - && KStream.class.isAssignableFrom(methodParameter.getParameterType()); - } - - static void closeDlqProducerFactories(KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - StreamsBuilderFactoryBean streamsBuilderFactoryBean) { - - final List> dlqProducerFactories = - kafkaStreamsBindingInformationCatalogue.getDlqProducerFactory(streamsBuilderFactoryBean); - - if (!CollectionUtils.isEmpty(dlqProducerFactories)) { - for (ProducerFactory producerFactory : dlqProducerFactories) { - try { - ((DisposableBean) producerFactory).destroy(); - } - catch (Exception exception) { - throw new IllegalStateException(exception); - } - } - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBindingInformationCatalogue.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBindingInformationCatalogue.java deleted file mode 100644 index 745bdc22b..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBindingInformationCatalogue.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.KStream; - -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.core.ResolvableType; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.util.CollectionUtils; - -/** - * A catalogue that provides binding information for Kafka Streams target types such as - * KStream. It also keeps a catalogue for the underlying {@link StreamsBuilderFactoryBean} - * and {@link StreamsConfig} associated with various - * Kafka Streams functions in the - * {@link org.springframework.context.ApplicationContext}. - * - * @author Soby Chacko - */ -public class KafkaStreamsBindingInformationCatalogue { - - private final Map, BindingProperties> bindingProperties = new ConcurrentHashMap<>(); - - private final Map, KafkaStreamsConsumerProperties> consumerProperties = new ConcurrentHashMap<>(); - - private final Map streamsBuilderFactoryBeanPerBinding = new HashMap<>(); - - private final Map> consumerPropertiesPerSbfb = new HashMap<>(); - - private final Map outboundKStreamResolvables = new HashMap<>(); - - private final Map, Serde> keySerdeInfo = new HashMap<>(); - - private final Map bindingNamesPerTarget = new HashMap<>(); - - private final Map previousKafkaStreamsPerApplicationId = new HashMap<>(); - - private final Map>> dlqProducerFactories = new HashMap<>(); - - /** - * For a given bounded {@link KStream}, retrieve it's corresponding destination on the - * broker. - * @param bindingTarget binding target for KStream - * @return destination topic on Kafka - */ - String getDestination(KStream bindingTarget) { - BindingProperties bindingProperties = this.bindingProperties.get(bindingTarget); - return bindingProperties.getDestination(); - } - - /** - * Is native decoding is enabled on this {@link KStream}. - * @param bindingTarget binding target for KStream - * @return true if native decoding is enabled, fasle otherwise. - */ - boolean isUseNativeDecoding(KStream bindingTarget) { - BindingProperties bindingProperties = this.bindingProperties.get(bindingTarget); - if (bindingProperties.getConsumer() == null) { - bindingProperties.setConsumer(new ConsumerProperties()); - } - return bindingProperties.getConsumer().isUseNativeDecoding(); - } - - /** - * Is DLQ enabled for this {@link KStream}. - * @param bindingTarget binding target for KStream - * @return true if DLQ is enabled, false otherwise. - */ - boolean isDlqEnabled(KStream bindingTarget) { - return this.consumerProperties.get(bindingTarget).isEnableDlq(); - } - - /** - * Retrieve the content type associated with a given {@link KStream}. - * @param bindingTarget binding target for KStream - * @return content Type associated. - */ - String getContentType(KStream bindingTarget) { - BindingProperties bindingProperties = this.bindingProperties.get(bindingTarget); - return bindingProperties.getContentType(); - } - - /** - * Register a cache for bounded KStream -> {@link BindingProperties}. - * @param bindingTarget binding target for KStream - * @param bindingProperties {@link BindingProperties} for this KStream - */ - void registerBindingProperties(KStream bindingTarget, - BindingProperties bindingProperties) { - if (bindingProperties != null) { - this.bindingProperties.put(bindingTarget, bindingProperties); - } - } - - /** - * Register a cache for bounded KStream -> {@link KafkaStreamsConsumerProperties}. - * @param bindingTarget binding target for KStream - * @param kafkaStreamsConsumerProperties consumer properties for this KStream - */ - void registerConsumerProperties(KStream bindingTarget, - KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties) { - if (kafkaStreamsConsumerProperties != null) { - this.consumerProperties.put(bindingTarget, kafkaStreamsConsumerProperties); - } - } - - Set getStreamsBuilderFactoryBeans() { - return new HashSet<>(this.streamsBuilderFactoryBeanPerBinding.values()); - } - - void addStreamBuilderFactoryPerBinding(String binding, StreamsBuilderFactoryBean streamsBuilderFactoryBean) { - this.streamsBuilderFactoryBeanPerBinding.put(binding, streamsBuilderFactoryBean); - } - - void addConsumerPropertiesPerSbfb(StreamsBuilderFactoryBean streamsBuilderFactoryBean, ConsumerProperties consumerProperties) { - this.consumerPropertiesPerSbfb.computeIfAbsent(streamsBuilderFactoryBean, k -> new ArrayList<>()); - this.consumerPropertiesPerSbfb.get(streamsBuilderFactoryBean).add(consumerProperties); - } - - public Map> getConsumerPropertiesPerSbfb() { - return this.consumerPropertiesPerSbfb; - } - - Map getStreamsBuilderFactoryBeanPerBinding() { - return this.streamsBuilderFactoryBeanPerBinding; - } - - void addOutboundKStreamResolvable(Object key, ResolvableType outboundResolvable) { - this.outboundKStreamResolvables.put(key, outboundResolvable); - } - - ResolvableType getOutboundKStreamResolvable(Object key) { - return outboundKStreamResolvables.get(key); - } - - /** - * Adding a mapping for KStream target to its corresponding KeySerde. - * This is used for sending to DLQ when deserialization fails. See {@link KafkaStreamsMessageConversionDelegate} - * for details. - * - * @param kStreamTarget target KStream - * @param keySerde Serde used for the key - */ - void addKeySerde(KStream kStreamTarget, Serde keySerde) { - this.keySerdeInfo.put(kStreamTarget, keySerde); - } - - Serde getKeySerde(KStream kStreamTarget) { - return this.keySerdeInfo.get(kStreamTarget); - } - - - Map, BindingProperties> getBindingProperties() { - return bindingProperties; - } - - Map, KafkaStreamsConsumerProperties> getConsumerProperties() { - return consumerProperties; - } - - void addBindingNamePerTarget(Object target, String bindingName) { - this.bindingNamesPerTarget.put(target, bindingName); - } - - String bindingNamePerTarget(Object target) { - return this.bindingNamesPerTarget.get(target); - } - - public List> getDlqProducerFactories() { - return this.dlqProducerFactories.values() - .stream() - .flatMap(List::stream) - .collect(Collectors.toList()); - } - - public List> getDlqProducerFactory(StreamsBuilderFactoryBean streamsBuilderFactoryBean) { - return this.dlqProducerFactories.get(streamsBuilderFactoryBean); - } - - public void addDlqProducerFactory(StreamsBuilderFactoryBean streamsBuilderFactoryBean, - ProducerFactory producerFactory) { - List> producerFactories = this.dlqProducerFactories.get(streamsBuilderFactoryBean); - if (CollectionUtils.isEmpty(producerFactories)) { - producerFactories = new ArrayList<>(); - this.dlqProducerFactories.put(streamsBuilderFactoryBean, producerFactories); - } - producerFactories.add(producerFactory); - } - - /** - * Caching the previous KafkaStreams for the application.id when binding is stopped through actuator. - * See https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - * - * @param applicationId application.id - * @param kafkaStreams {@link KafkaStreams} object - */ - public void addPreviousKafkaStreamsForApplicationId(String applicationId, KafkaStreams kafkaStreams) { - this.previousKafkaStreamsPerApplicationId.put(applicationId, kafkaStreams); - } - - /** - * Remove the previously cached KafkaStreams object. - * See https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - * - * @param applicationId application.id - */ - public void removePreviousKafkaStreamsForApplicationId(String applicationId) { - this.previousKafkaStreamsPerApplicationId.remove(applicationId); - } - - /** - * Get all stopped KafkaStreams objects through actuator binding stop. - * See https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1165 - * - * @return stopped KafkaStreams objects map - */ - public Map getStoppedKafkaStreams() { - return this.previousKafkaStreamsPerApplicationId; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsFunctionProcessor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsFunctionProcessor.java deleted file mode 100644 index 1511ee954..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsFunctionProcessor.java +++ /dev/null @@ -1,572 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.Topology; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.cloud.stream.binder.kafka.streams.function.KafkaStreamsBindableProxyFactory; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.function.FunctionConstants; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.core.ResolvableType; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.config.StreamsBuilderFactoryBeanConfigurer; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -/** - * @author Soby Chacko - * @author Byungjun You - * @since 2.2.0 - */ -public class KafkaStreamsFunctionProcessor extends AbstractKafkaStreamsBinderProcessor implements BeanFactoryAware { - - private static final Log LOG = LogFactory.getLog(KafkaStreamsFunctionProcessor.class); - private static final String OUTBOUND = "outbound"; - - private final BindingServiceProperties bindingServiceProperties; - private final Map methodStreamsBuilderFactoryBeanMap = new HashMap<>(); - private final KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties; - private final KeyValueSerdeResolver keyValueSerdeResolver; - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - private final KafkaStreamsMessageConversionDelegate kafkaStreamsMessageConversionDelegate; - - private BeanFactory beanFactory; - private StreamFunctionProperties streamFunctionProperties; - private KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties; - StreamsBuilderFactoryBeanConfigurer customizer; - ConfigurableEnvironment environment; - - public KafkaStreamsFunctionProcessor(BindingServiceProperties bindingServiceProperties, - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties, - KeyValueSerdeResolver keyValueSerdeResolver, - KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - KafkaStreamsMessageConversionDelegate kafkaStreamsMessageConversionDelegate, - CleanupConfig cleanupConfig, - StreamFunctionProperties streamFunctionProperties, - KafkaStreamsBinderConfigurationProperties kafkaStreamsBinderConfigurationProperties, - StreamsBuilderFactoryBeanConfigurer customizer, ConfigurableEnvironment environment) { - super(bindingServiceProperties, kafkaStreamsBindingInformationCatalogue, kafkaStreamsExtendedBindingProperties, - keyValueSerdeResolver, cleanupConfig); - this.bindingServiceProperties = bindingServiceProperties; - this.kafkaStreamsExtendedBindingProperties = kafkaStreamsExtendedBindingProperties; - this.keyValueSerdeResolver = keyValueSerdeResolver; - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - this.kafkaStreamsMessageConversionDelegate = kafkaStreamsMessageConversionDelegate; - this.streamFunctionProperties = streamFunctionProperties; - this.kafkaStreamsBinderConfigurationProperties = kafkaStreamsBinderConfigurationProperties; - this.customizer = customizer; - this.environment = environment; - } - - private Map buildTypeMap(ResolvableType resolvableType, - KafkaStreamsBindableProxyFactory kafkaStreamsBindableProxyFactory, - Method method, String functionName) { - Map resolvableTypeMap = new LinkedHashMap<>(); - if (method != null) { // Component functional bean. - final ResolvableType firstMethodParameter = ResolvableType.forMethodParameter(method, 0); - ResolvableType currentOutputGeneric = ResolvableType.forMethodReturnType(method); - - final Set inputs = new LinkedHashSet<>(kafkaStreamsBindableProxyFactory.getInputs()); - final Iterator iterator = inputs.iterator(); - populateResolvableTypeMap(firstMethodParameter, resolvableTypeMap, iterator, method, functionName); - - final Class outputRawclass = currentOutputGeneric.getRawClass(); - traverseReturnTypeForComponentBeans(resolvableTypeMap, currentOutputGeneric, inputs, iterator, outputRawclass); - } - else if (resolvableType != null && resolvableType.getRawClass() != null) { - int inputCount = 1; - - ResolvableType currentOutputGeneric; - if (resolvableType.getRawClass().isAssignableFrom(BiFunction.class) || - resolvableType.getRawClass().isAssignableFrom(BiConsumer.class)) { - inputCount = 2; - currentOutputGeneric = resolvableType.getGeneric(2); - } - else { - currentOutputGeneric = resolvableType.getGeneric(1); - } - while (currentOutputGeneric.getRawClass() != null && functionOrConsumerFound(currentOutputGeneric)) { - inputCount++; - currentOutputGeneric = currentOutputGeneric.getGeneric(1); - } - final Set inputs = new LinkedHashSet<>(kafkaStreamsBindableProxyFactory.getInputs()); - - final Iterator iterator = inputs.iterator(); - - populateResolvableTypeMap(resolvableType, resolvableTypeMap, iterator); - - ResolvableType iterableResType = resolvableType; - int i = resolvableType.getRawClass().isAssignableFrom(BiFunction.class) || - resolvableType.getRawClass().isAssignableFrom(BiConsumer.class) ? 2 : 1; - ResolvableType outboundResolvableType; - if (i == inputCount) { - outboundResolvableType = iterableResType.getGeneric(i); - } - else { - while (i < inputCount && iterator.hasNext()) { - iterableResType = iterableResType.getGeneric(1); - if (iterableResType.getRawClass() != null && - functionOrConsumerFound(iterableResType)) { - populateResolvableTypeMap(iterableResType, resolvableTypeMap, iterator); - } - i++; - } - outboundResolvableType = iterableResType.getGeneric(1); - } - resolvableTypeMap.put(OUTBOUND, outboundResolvableType); - } - return resolvableTypeMap; - } - - private void traverseReturnTypeForComponentBeans(Map resolvableTypeMap, ResolvableType currentOutputGeneric, - Set inputs, Iterator iterator, Class outputRawclass) { - if (outputRawclass != null && !outputRawclass.equals(Void.TYPE)) { - ResolvableType iterableResType = currentOutputGeneric; - int i = 1; - // Traverse through the return signature. - while (i < inputs.size() && iterator.hasNext()) { - if (iterableResType.getRawClass() != null && - functionOrConsumerFound(iterableResType)) { - populateResolvableTypeMap(iterableResType, resolvableTypeMap, iterator); - } - iterableResType = iterableResType.getGeneric(1); - i++; - } - if (iterableResType.getRawClass() != null && KStream.class.isAssignableFrom(iterableResType.getRawClass())) { - resolvableTypeMap.put(OUTBOUND, iterableResType); - } - } - } - - private boolean functionOrConsumerFound(ResolvableType iterableResType) { - return iterableResType.getRawClass().equals(Function.class) || - iterableResType.getRawClass().equals(Consumer.class); - } - - private void populateResolvableTypeMap(ResolvableType resolvableType, Map resolvableTypeMap, - Iterator iterator) { - final String next = iterator.next(); - resolvableTypeMap.put(next, resolvableType.getGeneric(0)); - if (resolvableType.getRawClass() != null && - (resolvableType.getRawClass().isAssignableFrom(BiFunction.class) || - resolvableType.getRawClass().isAssignableFrom(BiConsumer.class)) - && iterator.hasNext()) { - resolvableTypeMap.put(iterator.next(), resolvableType.getGeneric(1)); - } - } - - private void populateResolvableTypeMap(ResolvableType resolvableType, Map resolvableTypeMap, - Iterator iterator, Method method, String functionName) { - final String next = iterator.next(); - resolvableTypeMap.put(next, resolvableType); - if (method != null) { - final Object bean = beanFactory.getBean(functionName); - if (BiFunction.class.isAssignableFrom(bean.getClass()) || BiConsumer.class.isAssignableFrom(bean.getClass())) { - resolvableTypeMap.put(iterator.next(), ResolvableType.forMethodParameter(method, 1)); - } - } - } - - private ResolvableType checkOutboundForComposedFunctions( - ResolvableType outputResolvableType) { - - ResolvableType currentOutputGeneric; - - if (outputResolvableType.getRawClass() != null && outputResolvableType.getRawClass().isAssignableFrom(BiFunction.class)) { - currentOutputGeneric = outputResolvableType.getGeneric(2); - } - else { - currentOutputGeneric = outputResolvableType.getGeneric(1); - } - while (currentOutputGeneric.getRawClass() != null && functionOrConsumerFound(currentOutputGeneric)) { - currentOutputGeneric = currentOutputGeneric.getGeneric(1); - } - return currentOutputGeneric; - } - - /** - * This method must be kept stateless. In the case of multiple function beans in an application, - * isolated {@link KafkaStreamsBindableProxyFactory} instances are passed in separately for those functions. If the - * state is shared between invocations, that will create potential race conditions. Hence, invocations of this method - * should not be dependent on state modified by a previous invocation. - * - * @param resolvableType type of the binding - * @param functionName bean name of the function - * @param kafkaStreamsBindableProxyFactory bindable proxy factory for the Kafka Streams type - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void setupFunctionInvokerForKafkaStreams(ResolvableType resolvableType, String functionName, - KafkaStreamsBindableProxyFactory kafkaStreamsBindableProxyFactory, Method method, - ResolvableType outputResolvableType, - String... composedFunctionNames) { - final Map resolvableTypes = buildTypeMap(resolvableType, - kafkaStreamsBindableProxyFactory, method, functionName); - - ResolvableType outboundResolvableType; - if (outputResolvableType != null) { - outboundResolvableType = checkOutboundForComposedFunctions(outputResolvableType); - resolvableTypes.remove(OUTBOUND); - } - else { - outboundResolvableType = resolvableTypes.remove(OUTBOUND); - } - - Object[] adaptedInboundArguments = adaptAndRetrieveInboundArguments(resolvableTypes, functionName); - try { - if (resolvableType.getRawClass() != null && resolvableType.getRawClass().equals(Consumer.class)) { - Consumer consumer = (Consumer) this.beanFactory.getBean(functionName); - consumer.accept(adaptedInboundArguments[0]); - } - else if (resolvableType.getRawClass() != null && resolvableType.getRawClass().equals(BiConsumer.class)) { - BiConsumer biConsumer = (BiConsumer) this.beanFactory.getBean(functionName); - biConsumer.accept(adaptedInboundArguments[0], adaptedInboundArguments[1]); - } - else if (method != null) { // Handling component functional beans - final Object bean = beanFactory.getBean(functionName); - if (Consumer.class.isAssignableFrom(bean.getClass())) { - ((Consumer) bean).accept(adaptedInboundArguments[0]); - } - else if (BiConsumer.class.isAssignableFrom(bean.getClass())) { - ((BiConsumer) bean).accept(adaptedInboundArguments[0], adaptedInboundArguments[1]); - } - else if (Function.class.isAssignableFrom(bean.getClass()) || BiFunction.class.isAssignableFrom(bean.getClass())) { - Object result; - if (BiFunction.class.isAssignableFrom(bean.getClass())) { - result = ((BiFunction) bean).apply(adaptedInboundArguments[0], adaptedInboundArguments[1]); - } - else { - result = ((Function) bean).apply(adaptedInboundArguments[0]); - } - result = handleCurriedFunctions(adaptedInboundArguments, result); - if (result != null) { - final Set outputs = new TreeSet<>(kafkaStreamsBindableProxyFactory.getOutputs()); - final Iterator outboundDefinitionIterator = outputs.iterator(); - if (result.getClass().isArray()) { - final String initialInput = resolvableTypes.keySet().iterator().next(); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = - this.kafkaStreamsBindingInformationCatalogue.getStreamsBuilderFactoryBeanPerBinding().get(initialInput); - handleKStreamArrayOutbound(resolvableType, functionName, kafkaStreamsBindableProxyFactory, - outboundResolvableType, (Object[]) result, streamsBuilderFactoryBean); - } - else { - if (KTable.class.isAssignableFrom(result.getClass())) { - handleSingleKStreamOutbound(resolvableTypes, outboundResolvableType != null ? - outboundResolvableType : resolvableType.getGeneric(1), ((KTable) result).toStream(), outboundDefinitionIterator); - } - else { - handleSingleKStreamOutbound(resolvableTypes, outboundResolvableType, (KStream) result, outboundDefinitionIterator); - } - } - } - } - } - else { - Object result = null; - if (resolvableType.getRawClass() != null && resolvableType.getRawClass().equals(BiFunction.class)) { - if (composedFunctionNames != null && composedFunctionNames.length > 0) { - result = handleComposedFunctions(adaptedInboundArguments, result, composedFunctionNames); - } - else { - BiFunction biFunction = (BiFunction) beanFactory.getBean(functionName); - result = biFunction.apply(adaptedInboundArguments[0], adaptedInboundArguments[1]); - result = handleCurriedFunctions(adaptedInboundArguments, result); - } - } - else { - if (composedFunctionNames != null && composedFunctionNames.length > 0) { - result = handleComposedFunctions(adaptedInboundArguments, result, composedFunctionNames); - } - else { - Function function = (Function) beanFactory.getBean(functionName); - result = function.apply(adaptedInboundArguments[0]); - result = handleCurriedFunctions(adaptedInboundArguments, result); - } - } - if (result != null) { - final Set outputs = new TreeSet<>(kafkaStreamsBindableProxyFactory.getOutputs()); - final Iterator outboundDefinitionIterator = outputs.iterator(); - - if (result.getClass().isArray()) { - final String initialInput = resolvableTypes.keySet().iterator().next(); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = - this.kafkaStreamsBindingInformationCatalogue.getStreamsBuilderFactoryBeanPerBinding().get(initialInput); - handleKStreamArrayOutbound(resolvableType, functionName, kafkaStreamsBindableProxyFactory, - outboundResolvableType, (Object[]) result, streamsBuilderFactoryBean); - } - else { - if (KTable.class.isAssignableFrom(result.getClass())) { - handleSingleKStreamOutbound(resolvableTypes, outboundResolvableType != null ? - outboundResolvableType : resolvableType.getGeneric(1), ((KTable) result).toStream(), outboundDefinitionIterator); - } - else { - handleSingleKStreamOutbound(resolvableTypes, outboundResolvableType != null ? - outboundResolvableType : resolvableType.getGeneric(1), (KStream) result, outboundDefinitionIterator); - } - } - } - } - } - catch (Exception ex) { - throw new BeanInitializationException("Cannot setup function invoker for this Kafka Streams function.", ex); - } - } - - @SuppressWarnings({"unchecked"}) - private Object handleComposedFunctions(Object[] adaptedInboundArguments, Object result, String... composedFunctionNames) { - Object bean = beanFactory.getBean(composedFunctionNames[0]); - if (BiFunction.class.isAssignableFrom(bean.getClass())) { - result = ((BiFunction) bean).apply(adaptedInboundArguments[0], adaptedInboundArguments[1]); - } - else if (Function.class.isAssignableFrom(bean.getClass())) { - result = ((Function) bean).apply(adaptedInboundArguments[0]); - } - // If the return is a curried function, apply it - result = handleCurriedFunctions(adaptedInboundArguments, result); - // Apply composed functions - return applyComposedFunctions(result, composedFunctionNames); - } - - @SuppressWarnings({"unchecked"}) - private Object applyComposedFunctions(Object result, String[] composedFunctionNames) { - for (int i = 1; i < composedFunctionNames.length; i++) { - final Object bean = beanFactory.getBean(composedFunctionNames[i]); - if (Consumer.class.isAssignableFrom(bean.getClass())) { - ((Consumer) bean).accept(result); - result = null; - } - else if (Function.class.isAssignableFrom(bean.getClass())) { - result = ((Function) bean).apply(result); - } - else { - throw new IllegalStateException("You can only compose functions of type either java.util.function.Function or java.util.function.Consumer."); - } - } - return result; - } - - @SuppressWarnings({"unchecked"}) - private Object handleCurriedFunctions(Object[] adaptedInboundArguments, Object result) { - int i = 1; - while (result instanceof Function || result instanceof Consumer) { - if (result instanceof Function function) { - result = function.apply(adaptedInboundArguments[i]); - } - else { - ((Consumer) result).accept(adaptedInboundArguments[i]); - result = null; - } - i++; - } - return result; - } - - private void handleSingleKStreamOutbound(Map resolvableTypes, ResolvableType outboundResolvableType, - KStream result, Iterator outboundDefinitionIterator) { - if (outboundDefinitionIterator.hasNext()) { - String outbound = outboundDefinitionIterator.next(); - Object targetBean = handleSingleKStreamOutbound(result, outbound); - kafkaStreamsBindingInformationCatalogue.addOutboundKStreamResolvable(targetBean, - outboundResolvableType); - - final String next = resolvableTypes.keySet().iterator().next(); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeanPerBinding().get(next); - this.kafkaStreamsBindingInformationCatalogue.addStreamBuilderFactoryPerBinding(outbound, streamsBuilderFactoryBean); - } - } - - private Object handleSingleKStreamOutbound(KStream result, String next) { - Object targetBean = this.applicationContext.getBean(next); - KStreamBoundElementFactory.KStreamWrapper - boundElement = (KStreamBoundElementFactory.KStreamWrapper) targetBean; - boundElement.wrap(result); - return targetBean; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void handleKStreamArrayOutbound(ResolvableType resolvableType, String functionName, - KafkaStreamsBindableProxyFactory kafkaStreamsBindableProxyFactory, - ResolvableType outboundResolvableType, Object[] result, - StreamsBuilderFactoryBean streamsBuilderFactoryBean) { - // Binding target as the output bindings were deferred in the KafkaStreamsBindableProxyFactory - // due to the fact that it didn't know the returned array size. At this point in the execution, - // we know exactly the number of outbound components (from the array length), so do the binding. - final int length = result.length; - - List outputBindings = getOutputBindings(functionName, length); - Iterator iterator = outputBindings.iterator(); - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - - for (Object o : result) { - String next = iterator.next(); - kafkaStreamsBindableProxyFactory.addOutputBinding(next, KStream.class); - RootBeanDefinition rootBeanDefinition1 = new RootBeanDefinition(); - rootBeanDefinition1.setInstanceSupplier(() -> kafkaStreamsBindableProxyFactory.getOutputHolders().get(next).getBoundTarget()); - registry.registerBeanDefinition(next, rootBeanDefinition1); - - Object targetBean = this.applicationContext.getBean(next); - - KStreamBoundElementFactory.KStreamWrapper - boundElement = (KStreamBoundElementFactory.KStreamWrapper) targetBean; - boundElement.wrap((KStream) o); - - kafkaStreamsBindingInformationCatalogue.addOutboundKStreamResolvable( - targetBean, outboundResolvableType != null ? outboundResolvableType : resolvableType.getGeneric(1)); - - this.kafkaStreamsBindingInformationCatalogue.addStreamBuilderFactoryPerBinding(next, streamsBuilderFactoryBean); - } - } - - private List getOutputBindings(String functionName, int outputs) { - List outputBindings = this.streamFunctionProperties.getOutputBindings(functionName); - List outputBindingNames = new ArrayList<>(); - if (!CollectionUtils.isEmpty(outputBindings)) { - outputBindingNames.addAll(outputBindings); - return outputBindingNames; - } - else { - for (int i = 0; i < outputs; i++) { - outputBindingNames.add(String.format("%s-%s-%d", functionName, FunctionConstants.DEFAULT_OUTPUT_SUFFIX, i)); - } - } - return outputBindingNames; - - } - - @SuppressWarnings({"unchecked"}) - private Object[] adaptAndRetrieveInboundArguments(Map stringResolvableTypeMap, - String functionName) { - Object[] arguments = new Object[stringResolvableTypeMap.size()]; - int i = 0; - for (String input : stringResolvableTypeMap.keySet()) { - Class parameterType = stringResolvableTypeMap.get(input).getRawClass(); - - if (input != null) { - Object targetBean = applicationContext.getBean(input); - BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(input); - //Retrieve the StreamsConfig created for this method if available. - //Otherwise, create the StreamsBuilderFactory and get the underlying config. - if (!this.methodStreamsBuilderFactoryBeanMap.containsKey(functionName)) { - StreamsBuilderFactoryBean streamsBuilderFactoryBean = buildStreamsBuilderAndRetrieveConfig(functionName, applicationContext, - input, kafkaStreamsBinderConfigurationProperties, customizer, this.environment, bindingProperties); - this.methodStreamsBuilderFactoryBeanMap.put(functionName, streamsBuilderFactoryBean); - } - try { - StreamsBuilderFactoryBean streamsBuilderFactoryBean = - this.methodStreamsBuilderFactoryBeanMap.get(functionName); - StreamsBuilder streamsBuilder = streamsBuilderFactoryBean.getObject(); - final String applicationId = streamsBuilderFactoryBean.getStreamsConfiguration().getProperty(StreamsConfig.APPLICATION_ID_CONFIG); - KafkaStreamsConsumerProperties extendedConsumerProperties = - this.kafkaStreamsExtendedBindingProperties.getExtendedConsumerProperties(input); - extendedConsumerProperties.setApplicationId(applicationId); - //get state store spec - - Serde keySerde = this.keyValueSerdeResolver.getInboundKeySerde(extendedConsumerProperties, stringResolvableTypeMap.get(input)); - LOG.info("Key Serde used for " + input + ": " + keySerde.getClass().getName()); - Serde valueSerde = bindingServiceProperties.getConsumerProperties(input).isUseNativeDecoding() ? - getValueSerde(input, extendedConsumerProperties, stringResolvableTypeMap.get(input)) : Serdes.ByteArray(); - LOG.info("Value Serde used for " + input + ": " + valueSerde.getClass().getName()); - final Topology.AutoOffsetReset autoOffsetReset = getAutoOffsetReset(input, extendedConsumerProperties); - - if (parameterType.isAssignableFrom(KStream.class)) { - KStream stream = getKStream(input, bindingProperties, extendedConsumerProperties, - streamsBuilder, keySerde, valueSerde, autoOffsetReset, i == 0); - KStreamBoundElementFactory.KStreamWrapper kStreamWrapper = - (KStreamBoundElementFactory.KStreamWrapper) targetBean; - //wrap the proxy created during the initial target type binding with real object (KStream) - kStreamWrapper.wrap((KStream) stream); - - this.kafkaStreamsBindingInformationCatalogue.addKeySerde((KStream) kStreamWrapper, keySerde); - - this.kafkaStreamsBindingInformationCatalogue.addStreamBuilderFactoryPerBinding(input, streamsBuilderFactoryBean); - this.kafkaStreamsBindingInformationCatalogue.addConsumerPropertiesPerSbfb(streamsBuilderFactoryBean, - bindingServiceProperties.getConsumerProperties(input)); - - if (KStream.class.isAssignableFrom(stringResolvableTypeMap.get(input).getRawClass())) { - final Class valueClass = - (stringResolvableTypeMap.get(input).getGeneric(1).getRawClass() != null) - ? (stringResolvableTypeMap.get(input).getGeneric(1).getRawClass()) : Object.class; - if (this.kafkaStreamsBindingInformationCatalogue.isUseNativeDecoding( - (KStream) kStreamWrapper)) { - arguments[i] = stream; - } - else { - arguments[i] = this.kafkaStreamsMessageConversionDelegate.deserializeOnInbound( - valueClass, stream); - } - } - - if (arguments[i] == null) { - arguments[i] = stream; - } - Assert.notNull(arguments[i], "Problems encountered while adapting the function argument."); - } - else { - handleKTableGlobalKTableInputs(arguments, i, input, parameterType, targetBean, streamsBuilderFactoryBean, - streamsBuilder, extendedConsumerProperties, keySerde, valueSerde, autoOffsetReset, i == 0); - } - i++; - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - } - return arguments; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsJaasConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsJaasConfiguration.java deleted file mode 100644 index 8ee3d5257..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsJaasConfiguration.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.io.IOException; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.stream.binder.kafka.properties.JaasLoginModuleConfiguration; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.security.jaas.KafkaJaasLoginModuleInitializer; - -/** - * Jaas configuration bean for Kafka Streams binder types. - * - * @author Soby Chacko - * @since 3.1.4 - */ -@Configuration -public class KafkaStreamsJaasConfiguration { - - @Bean - @ConditionalOnMissingBean(KafkaJaasLoginModuleInitializer.class) - public KafkaJaasLoginModuleInitializer jaasInitializer( - KafkaBinderConfigurationProperties configurationProperties) - throws IOException { - KafkaJaasLoginModuleInitializer kafkaJaasLoginModuleInitializer = new KafkaJaasLoginModuleInitializer(); - JaasLoginModuleConfiguration jaas = configurationProperties.getJaas(); - if (jaas != null) { - kafkaJaasLoginModuleInitializer.setLoginModule(jaas.getLoginModule()); - - KafkaJaasLoginModuleInitializer.ControlFlag controlFlag = jaas - .getControlFlag(); - - if (controlFlag != null) { - kafkaJaasLoginModuleInitializer.setControlFlag(controlFlag); - } - kafkaJaasLoginModuleInitializer.setOptions(jaas.getOptions()); - } - return kafkaJaasLoginModuleInitializer; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsMessageConversionDelegate.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsMessageConversionDelegate.java deleted file mode 100644 index 60cf9a5b8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsMessageConversionDelegate.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serializer; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.processor.Processor; -import org.apache.kafka.streams.processor.ProcessorContext; - -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Delegate for handling all framework level message conversions inbound and outbound on - * {@link KStream}. If native encoding is not enabled, then serialization will be - * performed on outbound messages based on a contentType. Similarly, if native decoding is - * not enabled, deserialization will be performed on inbound messages based on a - * contentType. Based on the contentType, a {@link MessageConverter} will be resolved. - * - * @author Soby Chacko - * @author Byungjun You - */ -public class KafkaStreamsMessageConversionDelegate { - - private static final Log LOG = LogFactory - .getLog(KafkaStreamsMessageConversionDelegate.class); - - private static final ThreadLocal> keyValueThreadLocal = new ThreadLocal<>(); - - private final CompositeMessageConverter compositeMessageConverter; - - private final SendToDlqAndContinue sendToDlqAndContinue; - - private final KafkaStreamsBindingInformationCatalogue kstreamBindingInformationCatalogue; - - private final KafkaStreamsBinderConfigurationProperties kstreamBinderConfigurationProperties; - - Exception[] failedWithDeserException = new Exception[1]; - - KafkaStreamsMessageConversionDelegate( - CompositeMessageConverter compositeMessageConverter, - SendToDlqAndContinue sendToDlqAndContinue, - KafkaStreamsBindingInformationCatalogue kstreamBindingInformationCatalogue, - KafkaStreamsBinderConfigurationProperties kstreamBinderConfigurationProperties) { - this.compositeMessageConverter = compositeMessageConverter; - this.sendToDlqAndContinue = sendToDlqAndContinue; - this.kstreamBindingInformationCatalogue = kstreamBindingInformationCatalogue; - this.kstreamBinderConfigurationProperties = kstreamBinderConfigurationProperties; - } - - /** - * Serialize {@link KStream} records on outbound based on contentType. - * @param outboundBindTarget outbound KStream target - * @return serialized KStream - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - public KStream serializeOnOutbound(KStream outboundBindTarget) { - String contentType = this.kstreamBindingInformationCatalogue - .getContentType(outboundBindTarget); - MessageConverter messageConverter = this.compositeMessageConverter; - final PerRecordContentTypeHolder perRecordContentTypeHolder = new PerRecordContentTypeHolder(); - - final KStream kStreamWithEnrichedHeaders = outboundBindTarget - .filter((k, v) -> v != null) - .mapValues((v) -> { - Message message = v instanceof Message m ? m - : MessageBuilder.withPayload(v).build(); - Map headers = new HashMap<>(message.getHeaders()); - if (StringUtils.hasText(contentType)) { - headers.put(MessageHeaders.CONTENT_TYPE, contentType); - } - MessageHeaders messageHeaders = new MessageHeaders(headers); - final Message convertedMessage = messageConverter.toMessage(message.getPayload(), messageHeaders); - perRecordContentTypeHolder.setContentType((String) messageHeaders.get(MessageHeaders.CONTENT_TYPE)); - return convertedMessage.getPayload(); - }); - - kStreamWithEnrichedHeaders.process(() -> new Processor() { - - ProcessorContext context; - - @Override - public void init(ProcessorContext context) { - this.context = context; - } - - @Override - public void process(Object key, Object value) { - if (perRecordContentTypeHolder.contentType != null) { - this.context.headers().remove(MessageHeaders.CONTENT_TYPE); - final Header header; - try { - header = new RecordHeader(MessageHeaders.CONTENT_TYPE, - new ObjectMapper().writeValueAsBytes(perRecordContentTypeHolder.contentType)); - this.context.headers().add(header); - } - catch (Exception e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Could not add content type header"); - } - } - perRecordContentTypeHolder.unsetContentType(); - } - } - - @Override - public void close() { - - } - }); - - return kStreamWithEnrichedHeaders; - } - - /** - * Deserialize incoming {@link KStream} based on content type. - * @param valueClass on KStream value - * @param bindingTarget inbound KStream target - * @return deserialized KStream - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public KStream deserializeOnInbound(Class valueClass, - KStream bindingTarget) { - MessageConverter messageConverter = this.compositeMessageConverter; - final PerRecordContentTypeHolder perRecordContentTypeHolder = new PerRecordContentTypeHolder(); - - resolvePerRecordContentType(bindingTarget, perRecordContentTypeHolder); - - // Deserialize using a branching strategy - KStream[] branch = bindingTarget.branch( - // First filter where the message is converted and return true if - // everything went well, return false otherwise. - (o, o2) -> { - boolean isValidRecord = false; - - try { - // if the record is a tombstone, ignore and exit from processing - // further. - if (o2 != null) { - if (o2 instanceof Message || o2 instanceof String - || o2 instanceof byte[]) { - Message m1 = null; - if (o2 instanceof Message message) { - m1 = perRecordContentTypeHolder.contentType != null - ? MessageBuilder.fromMessage(message) - .setHeader( - MessageHeaders.CONTENT_TYPE, - perRecordContentTypeHolder.contentType) - .build() - : message; - } - else { - m1 = perRecordContentTypeHolder.contentType != null - ? MessageBuilder.withPayload(o2).setHeader( - MessageHeaders.CONTENT_TYPE, - perRecordContentTypeHolder.contentType) - .build() - : MessageBuilder.withPayload(o2).build(); - } - convertAndSetMessage(o, valueClass, messageConverter, m1); - } - else { - keyValueThreadLocal.set(new KeyValue<>(o, o2)); - } - isValidRecord = true; - } - else { - LOG.info( - "Received a tombstone record. This will be skipped from further processing."); - } - } - catch (Exception e) { - LOG.warn( - "Deserialization has failed. This will be skipped from further processing.", - e); - // pass through - failedWithDeserException[0] = e; - } - return isValidRecord; - }, - // second filter that catches any messages for which an exception thrown - // in the first filter above. - (k, v) -> true); - // process errors from the second filter in the branch above. - processErrorFromDeserialization(bindingTarget, branch[1], failedWithDeserException); - - // first branch above is the branch where the messages are converted, let it go - // through further processing. - return branch[0].mapValues((o2) -> { - Object objectValue = keyValueThreadLocal.get().value; - keyValueThreadLocal.remove(); - return objectValue; - }); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void resolvePerRecordContentType(KStream outboundBindTarget, - PerRecordContentTypeHolder perRecordContentTypeHolder) { - outboundBindTarget.process(() -> new Processor() { - - ProcessorContext context; - - @Override - public void init(ProcessorContext context) { - this.context = context; - } - - @Override - public void process(Object key, Object value) { - final Headers headers = this.context.headers(); - final Iterable
contentTypes = headers - .headers(MessageHeaders.CONTENT_TYPE); - if (contentTypes != null && contentTypes.iterator().hasNext()) { - final String contentType = new String( - contentTypes.iterator().next().value()); - // remove leading and trailing quotes - final String cleanContentType = StringUtils.replace(contentType, "\"", - ""); - perRecordContentTypeHolder.setContentType(cleanContentType); - } - } - - @Override - public void close() { - - } - }); - } - - private void convertAndSetMessage(Object o, Class valueClass, - MessageConverter messageConverter, Message msg) { - Object result = valueClass.isAssignableFrom(msg.getPayload().getClass()) - ? msg.getPayload() : messageConverter.fromMessage(msg, valueClass); - - Assert.notNull(result, "Failed to convert message " + msg); - - keyValueThreadLocal.set(new KeyValue<>(o, result)); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void processErrorFromDeserialization(KStream bindingTarget, - KStream branch, Exception[] exception) { - branch.process(() -> new Processor() { - ProcessorContext context; - - @Override - public void init(ProcessorContext context) { - this.context = context; - } - - @Override - public void process(Object o, Object o2) { - // Only continue if the record was not a tombstone. - if (o2 != null) { - if (KafkaStreamsMessageConversionDelegate.this.kstreamBindingInformationCatalogue - .isDlqEnabled(bindingTarget)) { - if (o2 instanceof Message message) { - - // We need to convert the key to a byte[] before sending to DLQ. - Serde keySerde = kstreamBindingInformationCatalogue.getKeySerde(bindingTarget); - Serializer keySerializer = keySerde.serializer(); - byte[] keyBytes = keySerializer.serialize(null, o); - - ConsumerRecord consumerRecord = new ConsumerRecord(this.context.topic(), this.context.partition(), this.context.offset(), - keyBytes, message.getPayload()); - - KafkaStreamsMessageConversionDelegate.this.sendToDlqAndContinue - .sendToDlq(consumerRecord, exception[0]); - } - else { - ConsumerRecord consumerRecord = new ConsumerRecord(this.context.topic(), this.context.partition(), this.context.offset(), - o, o2); - KafkaStreamsMessageConversionDelegate.this.sendToDlqAndContinue - .sendToDlq(consumerRecord, exception[0]); - } - } - else if (KafkaStreamsMessageConversionDelegate.this.kstreamBinderConfigurationProperties - .getSerdeError() == KafkaStreamsBinderConfigurationProperties.SerdeError.logAndFail) { - throw new IllegalStateException("Inbound deserialization failed. " - + "Stopping further processing of records."); - } - else if (KafkaStreamsMessageConversionDelegate.this.kstreamBinderConfigurationProperties - .getSerdeError() == KafkaStreamsBinderConfigurationProperties.SerdeError.logAndContinue) { - // quietly passing through. No action needed, this is similar to - // log and continue. - LOG.error( - "Inbound deserialization failed. Skipping this record and continuing."); - } - } - } - - @Override - public void close() { - - } - }); - } - - private static class PerRecordContentTypeHolder { - - String contentType; - - void setContentType(String contentType) { - this.contentType = contentType; - } - - void unsetContentType() { - this.contentType = null; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsRegistry.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsRegistry.java deleted file mode 100644 index ced9ac5ca..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsRegistry.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsConfig; - -import org.springframework.kafka.config.StreamsBuilderFactoryBean; - -/** - * An internal registry for holding {@link KafkaStreams} objects maintained through - * {@link StreamsBuilderFactoryManager}. - * - * @author Soby Chacko - */ -public class KafkaStreamsRegistry { - - private final Map streamsBuilderFactoryBeanMap = new ConcurrentHashMap<>(); - - private final Set kafkaStreams = ConcurrentHashMap.newKeySet(); - - Set getKafkaStreams() { - Set currentlyRunningKafkaStreams = new HashSet<>(); - for (KafkaStreams ks : this.kafkaStreams) { - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = streamsBuilderFactoryBeanMap.get(ks); - if (streamsBuilderFactoryBean.isRunning()) { - currentlyRunningKafkaStreams.add(ks); - } - } - return currentlyRunningKafkaStreams; - } - - /** - * Register the {@link KafkaStreams} object created in the application. - * @param streamsBuilderFactoryBean {@link StreamsBuilderFactoryBean} - */ - void registerKafkaStreams(StreamsBuilderFactoryBean streamsBuilderFactoryBean) { - final KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - this.kafkaStreams.add(kafkaStreams); - this.streamsBuilderFactoryBeanMap.put(kafkaStreams, streamsBuilderFactoryBean); - } - - void unregisterKafkaStreams(KafkaStreams kafkaStreams) { - this.kafkaStreams.remove(kafkaStreams); - this.streamsBuilderFactoryBeanMap.remove(kafkaStreams); - } - - /** - * - * @param kafkaStreams {@link KafkaStreams} object - * @return Corresponding {@link StreamsBuilderFactoryBean}. - */ - StreamsBuilderFactoryBean streamBuilderFactoryBean(KafkaStreams kafkaStreams) { - return this.streamsBuilderFactoryBeanMap.get(kafkaStreams); - } - - public StreamsBuilderFactoryBean streamsBuilderFactoryBean(String applicationId) { - final Optional first = this.streamsBuilderFactoryBeanMap.values() - .stream() - .filter(streamsBuilderFactoryBean -> streamsBuilderFactoryBean.isRunning() && streamsBuilderFactoryBean - .getStreamsConfiguration().getProperty(StreamsConfig.APPLICATION_ID_CONFIG) - .equals(applicationId)) - .findFirst(); - return first.orElse(null); - } - - public List streamsBuilderFactoryBeans() { - return new ArrayList<>(this.streamsBuilderFactoryBeanMap.values()); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsVersionAgnosticTopologyInfoFacade.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsVersionAgnosticTopologyInfoFacade.java deleted file mode 100644 index 763030bc5..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsVersionAgnosticTopologyInfoFacade.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.StringJoiner; - -import org.apache.kafka.streams.KafkaStreams; - -import org.springframework.core.log.LogAccessor; -import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ReflectionUtils; - -/** - * A facade to access topology info for a Kafka Streams application in a version agnostic - * manner. - *

Before kafka-streams 3.1 the topology exists at 'KafkaStreams.internalTopologyBuilder'. - * Starting in kafka-streams 3.1 the topology exists at 'KafkaStreams.topologyMetadata'. - * - * @author Chris Bono - * @since 3.2.6 - */ -class KafkaStreamsVersionAgnosticTopologyInfoFacade { - - private final LogAccessor logger = new LogAccessor(KafkaStreamsVersionAgnosticTopologyInfoFacade.class); - - @Nullable - private Field topologyInfoField; - - @Nullable - private Method sourceTopicsForStoreMethod; - - private boolean sourceTopicsForStoreMethodHasTwoArgs; - - KafkaStreamsVersionAgnosticTopologyInfoFacade() { - this(KafkaStreams.class); - } - - KafkaStreamsVersionAgnosticTopologyInfoFacade(Class rootClass) { - - // First look for KafkaStreams.internalTopologyBuilder (exists in kafka-streams <= 3.0) - Field internalTopologyBuilderField = ReflectionUtils.findField(rootClass, "internalTopologyBuilder"); - if (internalTopologyBuilderField != null) { - internalTopologyBuilderField.setAccessible(true); - this.topologyInfoField = internalTopologyBuilderField; - this.sourceTopicsForStoreMethod = ReflectionUtils.findMethod(internalTopologyBuilderField.getType(), "sourceTopicsForStore", String.class); - } - - // Otherwise look for KafkaStreams.topologyMetadata (exists in kafka-streams >= 3.1) - if (this.sourceTopicsForStoreMethod == null) { - Field topologyMetadataField = ReflectionUtils.findField(rootClass, "topologyMetadata"); - if (topologyMetadataField != null) { - topologyMetadataField.setAccessible(true); - this.topologyInfoField = topologyMetadataField; - this.sourceTopicsForStoreMethod = ReflectionUtils.findMethod(topologyMetadataField.getType(), "sourceTopicsForStore", String.class); - if (this.sourceTopicsForStoreMethod == null) { - // The sourceTopicsForStore method has extra arg in kafka-streams >= 3.3 - this.sourceTopicsForStoreMethod = ReflectionUtils.findMethod(topologyMetadataField.getType(), "sourceTopicsForStore", String.class, String.class); - this.sourceTopicsForStoreMethodHasTwoArgs = true; - } - } - } - - if (this.sourceTopicsForStoreMethod != null) { - this.sourceTopicsForStoreMethod.setAccessible(true); - logger.info(() -> "Using " + methodDescription(this.sourceTopicsForStoreMethod)); - } - else { - logger.warn("Could not find 'topologyMetadata.sourceTopicsForStore' or 'internalTopologyBuilder.sourceTopicsForStore' " + - "from KafkaStreams class - will be unable to reason about state stores."); - } - } - - /** - * Determines if a state store is actually available to a KafkaStreams instance by - * querying the topology info source topics for the requested store. - * - * @param kafkaStreams the streams app - * @param storeName the name of the state store - * @return {@code true} if state store is available or {@code false} if the state store is - * not available or there was a problem reflecting on the topology info - */ - boolean streamsAppActuallyHasStore(KafkaStreams kafkaStreams, String storeName) { - if (this.sourceTopicsForStoreMethod == null) { - logger.warn("Unable to reason about state store because sourceTopicsForStore method was not found - returning false"); - return false; - } - try { - Object topologyInfo = ReflectionUtils.getField(this.topologyInfoField, kafkaStreams); - if (topologyInfo == null) { - logger.warn("Unable to reason about state store because topologyInfo field was null - returning false"); - return false; - } - Object[] args = this.sourceTopicsForStoreMethodHasTwoArgs ? - new Object[] { storeName, null } : new Object[] { storeName }; - Collection sourceTopicsForStore = (Collection) - ReflectionUtils.invokeMethod(this.sourceTopicsForStoreMethod, topologyInfo, args); - return !CollectionUtils.isEmpty(sourceTopicsForStore); - } - catch (Exception ex) { - logger.error(ex, () -> "Unable to reason about state store due to error: " + ex.getMessage() + " - returning false"); - } - return false; - } - - private String methodDescription(Method method) { - StringJoiner sj = new StringJoiner(",", method.getName() + "(", ")"); - for (Class parameterType : method.getParameterTypes()) { - sj.add(parameterType.getTypeName()); - } - return "method " + method.getDeclaringClass().getTypeName() + '.' + sj.toString(); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KeyValueSerdeResolver.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KeyValueSerdeResolver.java deleted file mode 100644 index 3d73fd2f6..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/KeyValueSerdeResolver.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.utils.Utils; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.beans.BeansException; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsProducerProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.util.StringUtils; - -/** - * Resolver for key and value Serde. - * - * On the inbound, if native decoding is enabled, then any deserialization on the value is - * handled by Kafka. First, we look for any key/value Serde set on the binding itself, if - * that is not available then look at the common Serde set at the global level. If that - * fails, it falls back to byte[]. If native decoding is disabled, then the binder will do - * the deserialization on value and ignore any Serde set for value and rely on the - * contentType provided. Keys are always deserialized at the broker. - * - * - * Same rules apply on the outbound. If native encoding is enabled, then value - * serialization is done at the broker using any binder level Serde for value, if not - * using common Serde, if not, then byte[]. If native encoding is disabled, then the - * binder will do serialization using a contentType. Keys are always serialized by the - * broker. - * - * For state store, use serdes class specified in - * {@link org.springframework.cloud.stream.binder.kafka.streams.annotations.KafkaStreamsStateStore} - * to create Serde accordingly. - * - * @author Soby Chacko - * @author Lei Chen - * @author Eduard DomΓ­nguez - * @author Chris Bono - */ -public class KeyValueSerdeResolver implements ApplicationContextAware { - - private final Map streamConfigGlobalProperties; - - private final KafkaStreamsBinderConfigurationProperties binderConfigurationProperties; - - private ConfigurableApplicationContext context; - - KeyValueSerdeResolver(Map streamConfigGlobalProperties, - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties) { - this.streamConfigGlobalProperties = streamConfigGlobalProperties; - this.binderConfigurationProperties = binderConfigurationProperties; - } - - /** - * Provide the {@link Serde} for inbound key. - * @param extendedConsumerProperties binding level extended - * {@link KafkaStreamsConsumerProperties} - * @return configurd {@link Serde} for the inbound key. - */ - public Serde getInboundKeySerde( - KafkaStreamsConsumerProperties extendedConsumerProperties) { - String keySerdeString = extendedConsumerProperties.getKeySerde(); - - return getKeySerde(keySerdeString, extendedConsumerProperties.getConfiguration()); - } - - public Serde getInboundKeySerde( - KafkaStreamsConsumerProperties extendedConsumerProperties, ResolvableType resolvableType) { - String keySerdeString = extendedConsumerProperties.getKeySerde(); - - return getKeySerde(keySerdeString, resolvableType, extendedConsumerProperties.getConfiguration()); - } - - /** - * Provide the {@link Serde} for inbound value. - * @param consumerProperties {@link ConsumerProperties} on binding - * @param extendedConsumerProperties binding level extended - * {@link KafkaStreamsConsumerProperties} - * @return configurd {@link Serde} for the inbound value. - */ - public Serde getInboundValueSerde(ConsumerProperties consumerProperties, - KafkaStreamsConsumerProperties extendedConsumerProperties) { - Serde valueSerde; - - String valueSerdeString = extendedConsumerProperties.getValueSerde(); - try { - if (consumerProperties != null && consumerProperties.isUseNativeDecoding()) { - valueSerde = getValueSerde(valueSerdeString, extendedConsumerProperties.getConfiguration()); - } - else { - valueSerde = Serdes.ByteArray(); - } - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - return valueSerde; - } - - public Serde getInboundValueSerde(ConsumerProperties consumerProperties, - KafkaStreamsConsumerProperties extendedConsumerProperties, - ResolvableType resolvableType) { - Serde valueSerde; - - String valueSerdeString = extendedConsumerProperties.getValueSerde(); - try { - if (consumerProperties != null && consumerProperties.isUseNativeDecoding()) { - valueSerde = getValueSerde(valueSerdeString, resolvableType, extendedConsumerProperties.getConfiguration()); - } - else { - valueSerde = Serdes.ByteArray(); - } - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - return valueSerde; - } - - /** - * Provide the {@link Serde} for outbound key. - * @param properties binding level extended {@link KafkaStreamsProducerProperties} - * @return configurd {@link Serde} for the outbound key. - */ - public Serde getOuboundKeySerde(KafkaStreamsProducerProperties properties) { - return getKeySerde(properties.getKeySerde(), properties.getConfiguration()); - } - - public Serde getOuboundKeySerde(KafkaStreamsProducerProperties properties, ResolvableType resolvableType) { - return getKeySerde(properties.getKeySerde(), resolvableType, properties.getConfiguration()); - } - - - /** - * Provide the {@link Serde} for outbound value. - * @param producerProperties {@link ProducerProperties} on binding - * @param kafkaStreamsProducerProperties binding level extended - * {@link KafkaStreamsProducerProperties} - * @return configurd {@link Serde} for the outbound value. - */ - public Serde getOutboundValueSerde(ProducerProperties producerProperties, - KafkaStreamsProducerProperties kafkaStreamsProducerProperties) { - Serde valueSerde; - try { - if (producerProperties.isUseNativeEncoding()) { - valueSerde = getValueSerde( - kafkaStreamsProducerProperties.getValueSerde(), kafkaStreamsProducerProperties.getConfiguration()); - } - else { - valueSerde = Serdes.ByteArray(); - } - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - return valueSerde; - } - - public Serde getOutboundValueSerde(ProducerProperties producerProperties, - KafkaStreamsProducerProperties kafkaStreamsProducerProperties, ResolvableType resolvableType) { - Serde valueSerde; - try { - if (producerProperties.isUseNativeEncoding()) { - valueSerde = getValueSerde( - kafkaStreamsProducerProperties.getValueSerde(), resolvableType, kafkaStreamsProducerProperties.getConfiguration()); - } - else { - valueSerde = Serdes.ByteArray(); - } - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - return valueSerde; - } - - /** - * Provide the {@link Serde} for state store. - * @param keySerdeString serde class used for key - * @return {@link Serde} for the state store key. - */ - public Serde getStateStoreKeySerde(String keySerdeString) { - return getKeySerde(keySerdeString, null); - } - - /** - * Provide the {@link Serde} for state store value. - * @param valueSerdeString serde class used for value - * @return {@link Serde} for the state store value. - */ - public Serde getStateStoreValueSerde(String valueSerdeString) { - try { - return getValueSerde(valueSerdeString, null); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - } - - private Serde getKeySerde(String keySerdeString, Map extendedConfiguration) { - Serde keySerde; - try { - if (StringUtils.hasText(keySerdeString)) { - keySerde = Utils.newInstance(keySerdeString, Serde.class); - } - else { - keySerde = getFallbackSerde("default.key.serde"); - } - keySerde.configure(combineStreamConfigProperties(extendedConfiguration), true); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - return keySerde; - } - - private Serde getKeySerde(String keySerdeString, ResolvableType resolvableType, Map extendedConfiguration) { - Serde keySerde; - try { - if (StringUtils.hasText(keySerdeString)) { - keySerde = Utils.newInstance(keySerdeString, Serde.class); - } - else { - keySerde = Serdes.ByteArray(); - if (resolvableType != null && - (isResolvalbeKafkaStreamsType(resolvableType) || isResolvableKStreamArrayType(resolvableType))) { - ResolvableType targetType = resolvableType.isArray() ? resolvableType.getComponentType().getGeneric(0) : resolvableType.getGeneric(0); - Serde fallbackSerde = getFallbackSerde("default.key.serde"); - keySerde = SerdeResolverUtils.resolveForType(this.context, targetType, fallbackSerde); - } - } - keySerde.configure(combineStreamConfigProperties(extendedConfiguration), true); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Serde class not found: ", ex); - } - return keySerde; - } - - private boolean isResolvableKStreamArrayType(ResolvableType resolvableType) { - return resolvableType.isArray() && - KStream.class.isAssignableFrom(resolvableType.getComponentType().getRawClass()); - } - - private boolean isResolvalbeKafkaStreamsType(ResolvableType resolvableType) { - return resolvableType.getRawClass() != null && (KStream.class.isAssignableFrom(resolvableType.getRawClass()) || KTable.class.isAssignableFrom(resolvableType.getRawClass()) || - GlobalKTable.class.isAssignableFrom(resolvableType.getRawClass())); - } - - private Serde getValueSerde(String valueSerdeString, Map extendedConfiguration) - throws ClassNotFoundException { - Serde valueSerde; - if (StringUtils.hasText(valueSerdeString)) { - valueSerde = Utils.newInstance(valueSerdeString, Serde.class); - } - else { - valueSerde = getFallbackSerde("default.value.serde"); - } - valueSerde.configure(combineStreamConfigProperties(extendedConfiguration), false); - return valueSerde; - } - - private Serde getFallbackSerde(String s) throws ClassNotFoundException { - return this.binderConfigurationProperties.getConfiguration() - .containsKey(s) - ? Utils.newInstance(this.binderConfigurationProperties - .getConfiguration().get(s), - Serde.class) - : Serdes.ByteArray(); - } - - private Serde getValueSerde(String valueSerdeString, ResolvableType resolvableType, Map extendedConfiguration) - throws ClassNotFoundException { - Serde valueSerde; - if (StringUtils.hasText(valueSerdeString)) { - valueSerde = Utils.newInstance(valueSerdeString, Serde.class); - } - else { - valueSerde = Serdes.ByteArray(); - if (resolvableType != null && ((isResolvalbeKafkaStreamsType(resolvableType)) || - (isResolvableKStreamArrayType(resolvableType)))) { - ResolvableType targetType = resolvableType.isArray() ? resolvableType.getComponentType().getGeneric(1) : resolvableType.getGeneric(1); - Serde fallbackSerde = getFallbackSerde("default.value.serde"); - valueSerde = SerdeResolverUtils.resolveForType(this.context, targetType, fallbackSerde); - } - } - valueSerde.configure(combineStreamConfigProperties(extendedConfiguration), false); - return valueSerde; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - context = (ConfigurableApplicationContext) applicationContext; - } - - private Map combineStreamConfigProperties(Map extendedConfiguration) { - if (extendedConfiguration != null && !extendedConfiguration.isEmpty()) { - Map streamConfiguration = new HashMap<>(this.streamConfigGlobalProperties); - streamConfiguration.putAll(extendedConfiguration); - return streamConfiguration; - } - else { - return this.streamConfigGlobalProperties; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/MultiBinderPropertiesConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/MultiBinderPropertiesConfiguration.java deleted file mode 100644 index c0a07f7a9..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/MultiBinderPropertiesConfiguration.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Soby Chacko - * @since 3.0.2 - */ -@Configuration -public class MultiBinderPropertiesConfiguration { - - @Bean - @ConfigurationProperties(prefix = "spring.cloud.stream.kafka.streams.binder") - @ConditionalOnBean(name = "outerContext") - public KafkaBinderConfigurationProperties binderConfigurationProperties(KafkaProperties kafkaProperties) { - return new KafkaStreamsBinderConfigurationProperties(kafkaProperties); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SendToDlqAndContinue.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SendToDlqAndContinue.java deleted file mode 100644 index 569ef8c4f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SendToDlqAndContinue.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.kafka.clients.consumer.ConsumerRecord; - -import org.springframework.kafka.listener.ConsumerRecordRecoverer; -import org.springframework.kafka.listener.DeadLetterPublishingRecoverer; - -/** - * Custom implementation for {@link ConsumerRecordRecoverer} that keeps a collection of - * recoverer objects per input topics. These topics might be per input binding or multiplexed - * topics in a single binding. - * - * @author Soby Chacko - * @since 2.0.0 - */ -public class SendToDlqAndContinue implements ConsumerRecordRecoverer { - - /** - * DLQ dispatcher per topic in the application context. The key here is not the actual - * DLQ topic but the incoming topic that caused the error. - */ - private Map dlqDispatchers = new HashMap<>(); - - /** - * For a given topic, send the key/value record to DLQ topic. - * - * @param consumerRecord consumer record - * @param exception exception - */ - public void sendToDlq(ConsumerRecord consumerRecord, Exception exception) { - DeadLetterPublishingRecoverer kafkaStreamsDlqDispatch = this.dlqDispatchers.get(consumerRecord.topic()); - kafkaStreamsDlqDispatch.accept(consumerRecord, exception); - } - - void addKStreamDlqDispatch(String topic, - DeadLetterPublishingRecoverer kafkaStreamsDlqDispatch) { - this.dlqDispatchers.put(topic, kafkaStreamsDlqDispatch); - } - - @Override - public void accept(ConsumerRecord consumerRecord, Exception e) { - this.dlqDispatchers.get(consumerRecord.topic()).accept(consumerRecord, e); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SerdeResolverUtils.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SerdeResolverUtils.java deleted file mode 100644 index c8ceb40e2..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SerdeResolverUtils.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; - -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.kafka.support.serializer.JsonSerde; -import org.springframework.lang.Nullable; - -/** - * Utility class that contains various methods to help resolve {@link Serde Serdes}. - * - * @author Chris Bono - * @since 4.0 - */ -abstract class SerdeResolverUtils { - - private static final Log LOG = LogFactory.getLog(SerdeResolverUtils.class); - - /** Classnames of the standard built-in Serdes supported in {@link Serdes#serdeFrom(Class)}. */ - private static final Set STANDARD_SERDE_CLASSNAMES = Set.of( - Serdes.String().getClass().getName(), - Serdes.Short().getClass().getName(), - Serdes.Integer().getClass().getName(), - Serdes.Long().getClass().getName(), - Serdes.Float().getClass().getName(), - Serdes.Double().getClass().getName(), - Serdes.ByteArray().getClass().getName(), - Serdes.ByteBuffer().getClass().getName(), - Serdes.Bytes().getClass().getName(), - Serdes.UUID().getClass().getName()); - - /** - * Return the {@code Serde} to use for the specified type using the following steps until a match is found. - *

    - *
  • the closest matching configured {@code Serde} bean if one exists
  • - *
  • the Kafka Streams built-in serde if the target type is one of the built-in types exposed by Kafka Streams - * (Integer, Long, Short, Double, Float, byte[], UUID and String) - *
  • the fallback serde if specified and not one of the Kafka Streams exposed type serdes
  • - *
  • the {@link JsonSerde} if the target type is not exactly {@code Object}
  • - *
  • the fallback as the last resort
  • - *
- * @param context the application context - * @param targetType the target type to find the serde for - * @param fallbackSerde the serde to use when no type can be inferred - * @return serde to use for the target type or {@code fallbackSerde} as outlined in the method description - */ - static Serde resolveForType(ConfigurableApplicationContext context, ResolvableType targetType, @Nullable Serde fallbackSerde) { - - Class genericRawClazz = targetType.getRawClass(); - - // We don't attempt to find a matching Serde for type '?' - just return fallback - if (genericRawClazz == null) { - return fallbackSerde; - } - - List matchingSerdes = beanNamesForMatchingSerdes(context, targetType); - if (!matchingSerdes.isEmpty()) { - return context.getBean(matchingSerdes.get(0), Serde.class); - } - - // Use standard serde for built-in types - Serde standardDefaultSerde = getStandardDefaultSerde(genericRawClazz); - if (standardDefaultSerde != null) { - return standardDefaultSerde; - } - - // Use fallback if specified and not from std defaults (we know from above that type is not std default - // so using a fallback that is std default type would not work) - if (fallbackSerde != null && !isSerdeFromStandardDefaults(fallbackSerde)) { - return fallbackSerde; - } - - // Use JsonSerde if type is not exactly Object - if (!genericRawClazz.isAssignableFrom((Object.class))) { - return new JsonSerde<>(genericRawClazz); - } - - // Finally, just resort to using the fallback - return fallbackSerde; - } - - private static Serde getStandardDefaultSerde(Class genericRawClazz) { - try { - return Serdes.serdeFrom(genericRawClazz); - } - catch (IllegalArgumentException ex) { - if (LOG.isTraceEnabled()) { - LOG.trace(ex); - } - } - return null; - } - - private static boolean isSerdeFromStandardDefaults(Serde serde) { - if (serde == null) { - return false; - } - return STANDARD_SERDE_CLASSNAMES.contains(serde.getClass().getName()); - } - - /** - * Find the names of all {@link Serde} beans that can be used for {@code targetType}. - * - * @param context the application context - * @param targetType the target type the serdes are being matched for - * @return list of bean names for matching serdes ordered by most specific match, or an empty list if no matches found - */ - static List beanNamesForMatchingSerdes(ConfigurableApplicationContext context, ResolvableType targetType) { - // We don't attempt to find a matching Serde for type '?' - if (targetType.getRawClass() == null) { - return Collections.emptyList(); - } - List matchingSerdes = new ArrayList<>(); - ResolvableType serdeType = ResolvableType.forClassWithGenerics(Serde.class, targetType); - String[] serdeBeanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), - serdeType, false, false); - Arrays.stream(serdeBeanNames).forEach((beanName) -> { - try { - BeanDefinition beanDefinition = context.getBeanFactory().getMergedBeanDefinition(beanName); - ResolvableType serdeBeanGeneric = beanDefinition.getResolvableType().getGeneric(0); - if (LOG.isDebugEnabled()) { - LOG.debug("Found matching Serde<" + serdeBeanGeneric.getType() + "> under beanName=" + beanName); - } - matchingSerdes.add(new SerdeWithSpecificityScore(calculateScore(targetType, serdeBeanGeneric), beanName)); - } - catch (Exception ex) { - LOG.warn("Failed introspecting Serde bean '" + beanName + "'", ex); - } - }); - if (!matchingSerdes.isEmpty()) { - return matchingSerdes.stream().sorted(Collections.reverseOrder()) - .map(SerdeWithSpecificityScore::getSerdeBeanName) - .collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - /** - * Calculate a score to indicate how specific of a match one resolvable type is to another. - *


Simple string comparison (the number of matching leading characters between two type strings) is used to - * calculate the score. This approach avoids the recursive nature of the possible generic types, and leverages - * the type strings returned from {@link ResolvableType#toString()} and {@link Type#getTypeName()} which already - * include the properly handled generic types. The score is a composite of both of these properties because the - * 'toString' value does not include bounds values and works as a tie-breaker to distinguish non-exact - * matches that are closer in nature. - *


Example: - *

{@code
-	 * -------------------------------------------------------------------------------------------------------
-	 * targetType:   Foo             toString='Foo'     typeName='Foo'
-	 * typeToCheck1: Foo             toString='Foo'     typeName='Foo'
-	 * typeToCheck2: Foo   toString='Foo'     typeName='Foo'
-	 * -------------------------------------------------------------------------------------------------------
-	 * }
- * - * If using only the 'toString' value then both types would have the same score. However, including the 'typeName' - * value in the score differentiates them - in this case it is clear that 'typeToCheck1' is a direct match. - * - * @param targetType the target type - * @param typeToScore the type to calculate a score for - * @return a score on how close of a match {@code typeToScore} is to {@code targetType} - the higher the score, - * the closer the match - */ - private static int calculateScore(ResolvableType targetType, ResolvableType typeToScore) { - int score = countLeadingMatchingChars(targetType.getType().getTypeName(), typeToScore.getType().getTypeName()); - score += countLeadingMatchingChars(targetType.toString(), typeToScore.toString()); - return score; - } - - private static int countLeadingMatchingChars(String s1, String s2) { - if (s1 == null || s2 == null) { - return 0; - } - int matchCount = 0; - for (int i = 0; i < s1.length() && i < s2.length(); i++) { - if (s1.charAt(i) != s2.charAt(i)) { - break; - } - matchCount++; - } - return matchCount; - } - - /** - * Private internal class used strictly to 'remember' a score for a serde and use it for sorting later. - */ - private static class SerdeWithSpecificityScore implements Comparable { - private final Integer score; - private final String serdeBeanName; - - SerdeWithSpecificityScore(Integer score, String serdeBeanName) { - this.score = Objects.requireNonNull(score); - this.serdeBeanName = Objects.requireNonNull(serdeBeanName); - } - - String getSerdeBeanName() { - return serdeBeanName; - } - - @Override - public int compareTo(SerdeWithSpecificityScore other) { - return this.score.compareTo(other.score); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SkipAndContinueExceptionHandler.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SkipAndContinueExceptionHandler.java deleted file mode 100644 index 6fff5a25e..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/SkipAndContinueExceptionHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.Map; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.streams.errors.DeserializationExceptionHandler; -import org.apache.kafka.streams.processor.ProcessorContext; - -/** - * - * {@link DeserializationExceptionHandler} that allows to silently skip - * deserialization exceptions and continue processing. - * - * @author Soby Chakco - * @since 3.1.2 - */ -public class SkipAndContinueExceptionHandler implements DeserializationExceptionHandler { - - @Override - public DeserializationExceptionHandler.DeserializationHandlerResponse handle(final ProcessorContext context, - final ConsumerRecord record, - final Exception exception) { - return DeserializationExceptionHandler.DeserializationHandlerResponse.CONTINUE; - } - - @Override - public void configure(final Map configs) { - // ignore - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/StoreQueryParametersCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/StoreQueryParametersCustomizer.java deleted file mode 100644 index 012451eb8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/StoreQueryParametersCustomizer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.apache.kafka.streams.StoreQueryParameters; -import org.apache.kafka.streams.state.QueryableStoreType; - -/** - * Interface for customizing {@link StoreQueryParameters}. - * - * There are instances, in which an application wants to customize the internal - * {@link StoreQueryParameters} object created by {@link InteractiveQueryService} - * in {@link InteractiveQueryService#getQueryableStore(String, QueryableStoreType)} method. - * Applications can provide an implementation for this customizer as a bean - * and {@link InteractiveQueryService} will be provisioned with that customizer. - * - * @param state store generic type - * - * @author Soby Chacko - * @since 4.0.1 - */ -@FunctionalInterface -public interface StoreQueryParametersCustomizer { - - /** - * Customizing the internal {@link StoreQueryParameters} used by the {@link InteractiveQueryService}. - * - * @param storeQueryParameters Original {@link StoreQueryParameters} to customize - * @return the customized {@link StoreQueryParameters} - */ - StoreQueryParameters customize(StoreQueryParameters storeQueryParameters); -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/StreamsBuilderFactoryManager.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/StreamsBuilderFactoryManager.java deleted file mode 100644 index c0fa13e49..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/StreamsBuilderFactoryManager.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.context.SmartLifecycle; -import org.springframework.kafka.KafkaException; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.streams.KafkaStreamsMicrometerListener; - -/** - * Iterate through all {@link StreamsBuilderFactoryBean} in the application context and - * start them. As each one completes starting, register the associated KafkaStreams object - * into {@link InteractiveQueryService}. - * - * This {@link SmartLifecycle} class ensures that the bean created from it is started very - * late through the bootstrap process by setting the phase value closer to - * Integer.MAX_VALUE. This is to guarantee that the {@link StreamsBuilderFactoryBean} on a - * function with multiple bindings is only started after all the binding phases have completed successfully. - * - * @author Soby Chacko - */ -public class StreamsBuilderFactoryManager implements SmartLifecycle { - - private final KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue; - - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - private final KafkaStreamsBinderMetrics kafkaStreamsBinderMetrics; - - private final KafkaStreamsMicrometerListener listener; - - private volatile boolean running; - - private final KafkaProperties kafkaProperties; - - StreamsBuilderFactoryManager(KafkaStreamsBindingInformationCatalogue kafkaStreamsBindingInformationCatalogue, - KafkaStreamsRegistry kafkaStreamsRegistry, - KafkaStreamsBinderMetrics kafkaStreamsBinderMetrics, - KafkaStreamsMicrometerListener listener, - KafkaProperties kafkaProperties) { - this.kafkaStreamsBindingInformationCatalogue = kafkaStreamsBindingInformationCatalogue; - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - this.kafkaStreamsBinderMetrics = kafkaStreamsBinderMetrics; - this.listener = listener; - this.kafkaProperties = kafkaProperties; - } - - @Override - public boolean isAutoStartup() { - return this.kafkaProperties == null || this.kafkaProperties.getStreams().isAutoStartup(); - } - - @Override - public void stop(Runnable callback) { - stop(); - if (callback != null) { - callback.run(); - } - } - - @Override - public synchronized void start() { - if (!this.running) { - try { - Set streamsBuilderFactoryBeans = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeans(); - for (StreamsBuilderFactoryBean streamsBuilderFactoryBean : streamsBuilderFactoryBeans) { - if (this.listener != null) { - streamsBuilderFactoryBean.addListener(this.listener); - } - // By default, we shut down the client if there is an uncaught exception in the application. - // Users can override this by customizing SBFB. See this issue for more details: - // https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1110 - streamsBuilderFactoryBean.setStreamsUncaughtExceptionHandler(exception -> - StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_CLIENT); - // Starting the stream. - final Map> bindingServicePropertiesPerSbfb = - this.kafkaStreamsBindingInformationCatalogue.getConsumerPropertiesPerSbfb(); - final List consumerProperties = bindingServicePropertiesPerSbfb.get(streamsBuilderFactoryBean); - final boolean autoStartupDisabledOnAtLeastOneConsumerBinding = consumerProperties.stream().anyMatch(consumerProperties1 -> !consumerProperties1.isAutoStartup()); - if (!autoStartupDisabledOnAtLeastOneConsumerBinding) { - streamsBuilderFactoryBean.start(); - this.kafkaStreamsRegistry.registerKafkaStreams(streamsBuilderFactoryBean); - } - } - if (this.kafkaStreamsBinderMetrics != null) { - this.kafkaStreamsBinderMetrics.addMetrics(streamsBuilderFactoryBeans); - } - this.running = true; - } - catch (Exception ex) { - throw new KafkaException("Could not start stream: ", ex); - } - } - } - - @Override - public synchronized void stop() { - if (this.running) { - try { - Set streamsBuilderFactoryBeans = this.kafkaStreamsBindingInformationCatalogue - .getStreamsBuilderFactoryBeans(); - int n = 0; - for (StreamsBuilderFactoryBean streamsBuilderFactoryBean : streamsBuilderFactoryBeans) { - streamsBuilderFactoryBean.removeListener(this.listener); - streamsBuilderFactoryBean.stop(); - } - for (ProducerFactory dlqProducerFactory : this.kafkaStreamsBindingInformationCatalogue.getDlqProducerFactories()) { - ((DisposableBean) dlqProducerFactory).destroy(); - } - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - finally { - this.running = false; - } - } - } - - @Override - public synchronized boolean isRunning() { - return this.running; - } - - @Override - public int getPhase() { - return Integer.MAX_VALUE - 100; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/endpoint/KafkaStreamsTopologyEndpoint.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/endpoint/KafkaStreamsTopologyEndpoint.java deleted file mode 100644 index e3e470bdb..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/endpoint/KafkaStreamsTopologyEndpoint.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.streams.endpoint; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.endpoint.annotation.Selector; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsRegistry; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.util.StringUtils; - -/** - * Actuator endpoint for topology description. - * - * @author Soby Chacko - * @since 3.0.4 - */ -@Endpoint(id = "kafkastreamstopology") -public class KafkaStreamsTopologyEndpoint { - - /** - * Topology not found message. - */ - public static final String NO_TOPOLOGY_FOUND_MSG = "No topology found for the given application ID"; - - private final KafkaStreamsRegistry kafkaStreamsRegistry; - - public KafkaStreamsTopologyEndpoint(KafkaStreamsRegistry kafkaStreamsRegistry) { - this.kafkaStreamsRegistry = kafkaStreamsRegistry; - } - - @ReadOperation - public List kafkaStreamsTopologies() { - final List streamsBuilderFactoryBeans = this.kafkaStreamsRegistry.streamsBuilderFactoryBeans(); - final List descs = new ArrayList<>(); - streamsBuilderFactoryBeans.stream() - .forEach(streamsBuilderFactoryBean -> - descs.add(streamsBuilderFactoryBean.getTopology().describe().toString())); - return descs; - } - - @ReadOperation - public String kafkaStreamsTopology(@Selector String applicationId) { - if (StringUtils.hasText(applicationId)) { - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = this.kafkaStreamsRegistry.streamsBuilderFactoryBean(applicationId); - if (streamsBuilderFactoryBean != null) { - return streamsBuilderFactoryBean.getTopology().describe().toString(); - } - else { - return NO_TOPOLOGY_FOUND_MSG; - } - } - return NO_TOPOLOGY_FOUND_MSG; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/endpoint/KafkaStreamsTopologyEndpointAutoConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/endpoint/KafkaStreamsTopologyEndpointAutoConfiguration.java deleted file mode 100644 index 2d3439c42..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/endpoint/KafkaStreamsTopologyEndpointAutoConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.streams.endpoint; - -import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderSupportAutoConfiguration; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsRegistry; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Soby Chacko - * @since 3.0.4 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = { - "org.springframework.boot.actuate.endpoint.annotation.Endpoint" }) -@AutoConfigureAfter({EndpointAutoConfiguration.class, KafkaStreamsBinderSupportAutoConfiguration.class}) -public class KafkaStreamsTopologyEndpointAutoConfiguration { - - @Bean - @ConditionalOnAvailableEndpoint - public KafkaStreamsTopologyEndpoint topologyEndpoint(KafkaStreamsRegistry kafkaStreamsRegistry) { - return new KafkaStreamsTopologyEndpoint(kafkaStreamsRegistry); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/FunctionDetectorCondition.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/FunctionDetectorCondition.java deleted file mode 100644 index fc10220a3..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/FunctionDetectorCondition.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.function; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderUtils; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.ResolvableType; -import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; - -/** - * Custom {@link org.springframework.context.annotation.Condition} that detects the presence - * of java.util.Function|Consumer beans. Used for Kafka Streams function support. - * - * @author Soby Chacko - * @since 2.2.0 - */ -public class FunctionDetectorCondition extends SpringBootCondition { - - private static final Log LOG = LogFactory.getLog(FunctionDetectorCondition.class); - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - if (context != null && context.getBeanFactory() != null) { - - String[] functionTypes = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), Function.class, true, false); - String[] consumerTypes = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), Consumer.class, true, false); - String[] biFunctionTypes = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), BiFunction.class, true, false); - String[] biConsumerTypes = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), BiConsumer.class, true, false); - - List functionComponents = new ArrayList<>(); - - functionComponents.addAll(Arrays.asList(functionTypes)); - functionComponents.addAll(Arrays.asList(consumerTypes)); - functionComponents.addAll(Arrays.asList(biFunctionTypes)); - functionComponents.addAll(Arrays.asList(biConsumerTypes)); - - List kafkaStreamsFunctions = pruneFunctionBeansForKafkaStreams(functionComponents, context); - if (!CollectionUtils.isEmpty(kafkaStreamsFunctions)) { - return ConditionOutcome.match("Matched. Function/BiFunction/Consumer beans found"); - } - else { - return ConditionOutcome.noMatch("No match. No Function/BiFunction/Consumer beans found"); - } - } - return ConditionOutcome.noMatch("No match. No Function/BiFunction/Consumer beans found"); - } - - private static List pruneFunctionBeansForKafkaStreams(List functionComponents, - ConditionContext context) { - final List prunedList = new ArrayList<>(); - - for (String key : functionComponents) { - final Class classObj = ClassUtils.resolveClassName(((AnnotatedBeanDefinition) - context.getBeanFactory().getBeanDefinition(key)) - .getMetadata().getClassName(), - ClassUtils.getDefaultClassLoader()); - try { - - Method[] methods = classObj.getDeclaredMethods(); - Optional kafkaStreamMethod = KafkaStreamsBinderUtils.findMethodWithName(key, methods); - if (kafkaStreamMethod.isEmpty()) { - // check any inherited methods - methods = classObj.getMethods(); - kafkaStreamMethod = KafkaStreamsBinderUtils.findMethodWithName(key, methods); - } - if (kafkaStreamMethod.isEmpty()) { - // check if the bean name is overridden. - final BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition(key); - final String factoryMethodName = beanDefinition.getFactoryMethodName(); - kafkaStreamMethod = KafkaStreamsBinderUtils.findMethodWithName(factoryMethodName, methods); - } - - if (kafkaStreamMethod.isPresent()) { - Method method = kafkaStreamMethod.get(); - ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, classObj); - final Class rawClass = resolvableType.getGeneric(0).getRawClass(); - if (rawClass == KStream.class || rawClass == KTable.class || rawClass == GlobalKTable.class) { - prunedList.add(key); - } - } - else { - //check if it is a @Component bean. - Optional componentBeanMethod = Arrays.stream(methods).filter( - m -> (m.getName().equals("apply") || m.getName().equals("accept")) - && isKafkaStreamsTypeFound(m)).findFirst(); - if (componentBeanMethod.isPresent()) { - Method method = componentBeanMethod.get(); - final ResolvableType resolvableType1 = ResolvableType.forMethodParameter(method, 0); - final Class rawClass = resolvableType1.getRawClass(); - if (rawClass == KStream.class || rawClass == KTable.class || rawClass == GlobalKTable.class) { - prunedList.add(key); - } - } - } - } - catch (Exception e) { - LOG.error("Function not found: " + key, e); - } - } - return prunedList; - } - - private static boolean isKafkaStreamsTypeFound(Method method) { - return KStream.class.isAssignableFrom(method.getParameters()[0].getType()) || - KTable.class.isAssignableFrom(method.getParameters()[0].getType()) || - GlobalKTable.class.isAssignableFrom(method.getParameters()[0].getType()); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBindableProxyFactory.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBindableProxyFactory.java deleted file mode 100644 index 80806e92b..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBindableProxyFactory.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.function; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.cloud.stream.binding.AbstractBindableProxyFactory; -import org.springframework.cloud.stream.binding.BoundTargetHolder; -import org.springframework.cloud.stream.function.FunctionConstants; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.core.ResolvableType; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -/** - * Kafka Streams specific target bindings proxy factory. See {@link AbstractBindableProxyFactory} for more details. - *

- * Targets bound by this factory: - *

- * {@link KStream} - * {@link KTable} - * {@link GlobalKTable} - *

- * This class looks at the Function bean's return signature as {@link ResolvableType} and introspect the individual types, - * binding them on the way. - *

- * All types on the {@link ResolvableType} are bound except for KStream[] array types on the outbound, which will be - * deferred for binding at a later stage. The reason for doing that is because in this class, we don't have any way to know - * the actual size in the returned array. That has to wait until the function is invoked and we get a result. - * - * @author Soby Chacko - * @since 3.0.0 - */ -public class KafkaStreamsBindableProxyFactory extends AbstractBindableProxyFactory implements InitializingBean, BeanFactoryAware { - - @Autowired - private StreamFunctionProperties streamFunctionProperties; - - private ResolvableType[] types; - - private Method method; - - private final String functionName; - - private BeanFactory beanFactory; - - public KafkaStreamsBindableProxyFactory(ResolvableType[] types, String functionName, Method method) { - super(types[0].getType().getClass()); - this.types = types; - this.functionName = functionName; - this.method = method; - } - - @Override - public void afterPropertiesSet() { - populateBindingTargetFactories(beanFactory); - Assert.notEmpty(KafkaStreamsBindableProxyFactory.this.bindingTargetFactories, - "'bindingTargetFactories' cannot be empty"); - - int resolvableTypeDepthCounter = 0; - boolean isKafkaStreamsType = this.types[0].getRawClass().isAssignableFrom(KStream.class) || - this.types[0].getRawClass().isAssignableFrom(KTable.class) || - this.types[0].getRawClass().isAssignableFrom(GlobalKTable.class); - ResolvableType argument = isKafkaStreamsType ? this.types[0] : this.types[0].getGeneric(resolvableTypeDepthCounter++); - List inputBindings = buildInputBindings(); - Iterator iterator = inputBindings.iterator(); - String next = iterator.next(); - bindInput(argument, next); - - // Check if its a component style bean. - if (method != null) { - final Object bean = beanFactory.getBean(functionName); - if (BiFunction.class.isAssignableFrom(bean.getClass()) || BiConsumer.class.isAssignableFrom(bean.getClass())) { - argument = ResolvableType.forMethodParameter(method, 1); - next = iterator.next(); - bindInput(argument, next); - } - } - // Normal functional bean - if (this.types[0].getRawClass() != null && - (this.types[0].getRawClass().isAssignableFrom(BiFunction.class) || - this.types[0].getRawClass().isAssignableFrom(BiConsumer.class))) { - argument = this.types[0].getGeneric(resolvableTypeDepthCounter++); - next = iterator.next(); - bindInput(argument, next); - } - ResolvableType outboundArgument; - if (method != null) { - outboundArgument = ResolvableType.forMethodReturnType(method); - } - else { - outboundArgument = this.types[0].getGeneric(resolvableTypeDepthCounter); - } - - while (isAnotherFunctionOrConsumerFound(outboundArgument)) { - //The function is a curried function. We should introspect the partial function chain hierarchy. - argument = outboundArgument.getGeneric(0); - String next1 = iterator.next(); - bindInput(argument, next1); - outboundArgument = outboundArgument.getGeneric(1); - } - - - final int lastTypeIndex = this.types.length - 1; - if (this.types.length > 1 && this.types[lastTypeIndex] != null && this.types[lastTypeIndex].getRawClass() != null) { - if (this.types[lastTypeIndex].getRawClass().isAssignableFrom(Function.class) || - this.types[lastTypeIndex].getRawClass().isAssignableFrom(Consumer.class)) { - outboundArgument = this.types[lastTypeIndex].getGeneric(1); - } - } - - if (outboundArgument != null && outboundArgument.getRawClass() != null && (!outboundArgument.isArray() && - (outboundArgument.getRawClass().isAssignableFrom(KStream.class) || - outboundArgument.getRawClass().isAssignableFrom(KTable.class)))) { //Allowing both KStream and KTable on the outbound. - // if the type is array, we need to do a late binding as we don't know the number of - // output bindings at this point in the flow. - - List outputBindings = streamFunctionProperties.getOutputBindings(this.functionName); - String outputBinding = null; - - if (!CollectionUtils.isEmpty(outputBindings)) { - Iterator outputBindingsIter = outputBindings.iterator(); - if (outputBindingsIter.hasNext()) { - outputBinding = outputBindingsIter.next(); - } - } - else { - outputBinding = String.format("%s-%s-0", this.functionName, FunctionConstants.DEFAULT_OUTPUT_SUFFIX); - } - Assert.isTrue(outputBinding != null, "output binding is not inferred."); - // We will only allow KStream targets on the outbound. If the user provides a KTable, - // we still use the KStreamBinder to send it through the outbound. - // In that case before sending, we do a cast from KTable to KStream. - // See KafkaStreamsFunctionsProcessor#setupFunctionInvokerForKafkaStreams for details. - KafkaStreamsBindableProxyFactory.this.outputHolders.put(outputBinding, - new BoundTargetHolder(getBindingTargetFactory(KStream.class) - .createOutput(outputBinding), true)); - String outputBinding1 = outputBinding; - RootBeanDefinition rootBeanDefinition1 = new RootBeanDefinition(); - rootBeanDefinition1.setInstanceSupplier(() -> outputHolders.get(outputBinding1).getBoundTarget()); - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - registry.registerBeanDefinition(outputBinding1, rootBeanDefinition1); - } - } - - private boolean isAnotherFunctionOrConsumerFound(ResolvableType arg1) { - return arg1 != null && !arg1.isArray() && arg1.getRawClass() != null && - (arg1.getRawClass().isAssignableFrom(Function.class) || arg1.getRawClass().isAssignableFrom(Consumer.class)); - } - - /** - * If the application provides the property spring.cloud.stream.function.inputBindings.functionName, - * that gets precedence. Otherwise, use functionName-input or functionName-input-0, functionName-input-1 and so on - * for multiple inputs. - * - * @return an ordered collection of input bindings to use - */ - private List buildInputBindings() { - List inputs = new ArrayList<>(); - List inputBindings = streamFunctionProperties.getInputBindings(this.functionName); - if (!CollectionUtils.isEmpty(inputBindings)) { - inputs.addAll(inputBindings); - return inputs; - } - int numberOfInputs = this.types[0].getRawClass() != null && - (this.types[0].getRawClass().isAssignableFrom(BiFunction.class) || - this.types[0].getRawClass().isAssignableFrom(BiConsumer.class)) ? 2 : getNumberOfInputs(); - - // For @Component style beans. - if (method != null) { - final ResolvableType returnType = ResolvableType.forMethodReturnType(method); - Object bean = beanFactory.containsBean(functionName) ? beanFactory.getBean(functionName) : null; - - if (bean != null && (BiFunction.class.isAssignableFrom(bean.getClass()) || BiConsumer.class.isAssignableFrom(bean.getClass()))) { - numberOfInputs = 2; - } - else if (returnType.getRawClass().isAssignableFrom(Function.class) || returnType.getRawClass().isAssignableFrom(Consumer.class)) { - numberOfInputs = 1; - ResolvableType arg1 = returnType; - - while (isAnotherFunctionOrConsumerFound(arg1)) { - arg1 = arg1.getGeneric(1); - numberOfInputs++; - } - } - } - - int i = 0; - while (i < numberOfInputs) { - inputs.add(String.format("%s-%s-%d", this.functionName, FunctionConstants.DEFAULT_INPUT_SUFFIX, i++)); - } - return inputs; - } - - private int getNumberOfInputs() { - int numberOfInputs = 1; - ResolvableType arg1 = this.types[0].getGeneric(1); - - while (isAnotherFunctionOrConsumerFound(arg1)) { - arg1 = arg1.getGeneric(1); - numberOfInputs++; - } - return numberOfInputs; - } - - private void bindInput(ResolvableType arg0, String inputName) { - if (arg0.getRawClass() != null) { - KafkaStreamsBindableProxyFactory.this.inputHolders.put(inputName, - new BoundTargetHolder(getBindingTargetFactory(arg0.getRawClass()) - .createInput(inputName), true)); - } - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(); - rootBeanDefinition.setInstanceSupplier(() -> inputHolders.get(inputName).getBoundTarget()); - registry.registerBeanDefinition(inputName, rootBeanDefinition); - } - - @Override - public Set getInputs() { - Set ins = new LinkedHashSet<>(); - this.inputHolders.forEach((s, BoundTargetHolder) -> ins.add(s)); - return ins; - } - - @Override - public Set getOutputs() { - Set outs = new LinkedHashSet<>(); - this.outputHolders.forEach((s, BoundTargetHolder) -> outs.add(s)); - return outs; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - public void addOutputBinding(String output, Class clazz) { - KafkaStreamsBindableProxyFactory.this.outputHolders.put(output, - new BoundTargetHolder(getBindingTargetFactory(clazz) - .createOutput(output), true)); - } - - public String getFunctionName() { - return functionName; - } - - public Map getOutputHolders() { - return outputHolders; - } -} - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionAutoConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionAutoConfiguration.java deleted file mode 100644 index 1befd6784..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionAutoConfiguration.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.function; - -//import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsFunctionProcessor; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; - -/** - * @author Soby Chacko - * @since 2.2.0 - */ -@Configuration(proxyBeanMethods = false) -public class KafkaStreamsFunctionAutoConfiguration { - - @Bean - @Conditional(FunctionDetectorCondition.class) - public KafkaStreamsFunctionProcessorInvoker kafkaStreamsFunctionProcessorInvoker( - KafkaStreamsFunctionBeanPostProcessor kafkaStreamsFunctionBeanPostProcessor, - KafkaStreamsFunctionProcessor kafkaStreamsFunctionProcessor, - KafkaStreamsBindableProxyFactory[] kafkaStreamsBindableProxyFactories, - StreamFunctionProperties streamFunctionProperties) { - return new KafkaStreamsFunctionProcessorInvoker(kafkaStreamsFunctionBeanPostProcessor.getResolvableTypes(), - kafkaStreamsFunctionProcessor, kafkaStreamsBindableProxyFactories, kafkaStreamsFunctionBeanPostProcessor.getMethods(), - streamFunctionProperties); - } - - @Bean - @Conditional(FunctionDetectorCondition.class) - public KafkaStreamsFunctionBeanPostProcessor kafkaStreamsFunctionBeanPostProcessor(StreamFunctionProperties streamFunctionProperties) { - return new KafkaStreamsFunctionBeanPostProcessor(streamFunctionProperties); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionBeanPostProcessor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionBeanPostProcessor.java deleted file mode 100644 index 8276eaecd..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionBeanPostProcessor.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.function; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderUtils; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.core.ResolvableType; -import org.springframework.util.ClassUtils; - -/** - * @author Soby Chacko - * @author James Forward - * - * @since 2.2.0 - */ -public class KafkaStreamsFunctionBeanPostProcessor implements InitializingBean, BeanFactoryAware { - - private static final Log LOG = LogFactory.getLog(KafkaStreamsFunctionBeanPostProcessor.class); - - private static final String[] EXCLUDE_FUNCTIONS = new String[]{"functionRouter", "sendToDlqAndContinue"}; - - private ConfigurableListableBeanFactory beanFactory; - private boolean onlySingleFunction; - private final Map resolvableTypeMap = new TreeMap<>(); - private final Map methods = new TreeMap<>(); - - private final StreamFunctionProperties streamFunctionProperties; - - private final Map kafkaStreamsOnlyResolvableTypes = new HashMap<>(); - private final Map kafakStreamsOnlyMethods = new HashMap<>(); - - public KafkaStreamsFunctionBeanPostProcessor(StreamFunctionProperties streamFunctionProperties) { - this.streamFunctionProperties = streamFunctionProperties; - } - - public Map getResolvableTypes() { - return this.resolvableTypeMap; - } - - public Map getMethods() { - return methods; - } - - @Override - public void afterPropertiesSet() { - String[] functionNames = this.beanFactory.getBeanNamesForType(Function.class); - String[] biFunctionNames = this.beanFactory.getBeanNamesForType(BiFunction.class); - String[] consumerNames = this.beanFactory.getBeanNamesForType(Consumer.class); - String[] biConsumerNames = this.beanFactory.getBeanNamesForType(BiConsumer.class); - - final Stream concat = Stream.concat( - Stream.concat(Stream.of(functionNames), Stream.of(consumerNames)), - Stream.concat(Stream.of(biFunctionNames), Stream.of(biConsumerNames))); - final List collect = concat.collect(Collectors.toList()); - collect.removeIf(s -> Arrays.stream(EXCLUDE_FUNCTIONS).anyMatch(t -> t.equals(s))); - collect.removeIf(Pattern.compile(".*_registration").asPredicate()); - - onlySingleFunction = collect.size() == 1; - collect.stream() - .forEach(this::extractResolvableTypes); - - kafkaStreamsOnlyResolvableTypes.keySet().forEach(k -> addResolvableTypeInfo(k, kafkaStreamsOnlyResolvableTypes.get(k))); - kafakStreamsOnlyMethods.keySet().forEach(k -> addResolvableTypeInfo(k, kafakStreamsOnlyMethods.get(k))); - - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - - final String definition = streamFunctionProperties.getDefinition(); - final String[] functionUnits = KafkaStreamsBinderUtils.deriveFunctionUnits(definition); - - final Set kafkaStreamsMethodNames = new HashSet<>(kafkaStreamsOnlyResolvableTypes.keySet()); - kafkaStreamsMethodNames.addAll(this.resolvableTypeMap.keySet()); - - if (functionUnits.length == 0) { - for (String s : getResolvableTypes().keySet()) { - ResolvableType[] resolvableTypes = new ResolvableType[]{getResolvableTypes().get(s)}; - RootBeanDefinition rootBeanDefinition = new RootBeanDefinition( - KafkaStreamsBindableProxyFactory.class); - registerKakaStreamsProxyFactory(registry, s, resolvableTypes, rootBeanDefinition); - } - } - else { - for (String functionUnit : functionUnits) { - if (functionUnit.contains("|")) { - final String[] composedFunctions = functionUnit.split("\\|"); - String derivedNameFromComposed = ""; - ResolvableType[] resolvableTypes = new ResolvableType[composedFunctions.length]; - - int i = 0; - boolean nonKafkaStreamsFunctionsFound = false; - - for (String split : composedFunctions) { - derivedNameFromComposed = derivedNameFromComposed.concat(split); - resolvableTypes[i++] = getResolvableTypes().get(split); - if (!kafkaStreamsMethodNames.contains(split)) { - nonKafkaStreamsFunctionsFound = true; - break; - } - } - if (!nonKafkaStreamsFunctionsFound) { - RootBeanDefinition rootBeanDefinition = new RootBeanDefinition( - KafkaStreamsBindableProxyFactory.class); - registerKakaStreamsProxyFactory(registry, derivedNameFromComposed, resolvableTypes, rootBeanDefinition); - } - } - else { - // Ensure that the function unit is a Kafka Streams function - if (kafkaStreamsMethodNames.contains(functionUnit)) { - ResolvableType[] resolvableTypes = new ResolvableType[]{getResolvableTypes().get(functionUnit)}; - RootBeanDefinition rootBeanDefinition = new RootBeanDefinition( - KafkaStreamsBindableProxyFactory.class); - registerKakaStreamsProxyFactory(registry, functionUnit, resolvableTypes, rootBeanDefinition); - } - } - } - } - } - - private void registerKakaStreamsProxyFactory(BeanDefinitionRegistry registry, String s, ResolvableType[] resolvableTypes, RootBeanDefinition rootBeanDefinition) { - rootBeanDefinition.getConstructorArgumentValues() - .addGenericArgumentValue(resolvableTypes); - rootBeanDefinition.getConstructorArgumentValues() - .addGenericArgumentValue(s); - rootBeanDefinition.getConstructorArgumentValues() - .addGenericArgumentValue(getMethods().get(s)); - registry.registerBeanDefinition("kafkaStreamsBindableProxyFactory-" + s, rootBeanDefinition); - } - - private void extractResolvableTypes(String key) { - final Class classObj = ClassUtils.resolveClassName(((AnnotatedBeanDefinition) - this.beanFactory.getBeanDefinition(key)) - .getMetadata().getClassName(), - ClassUtils.getDefaultClassLoader()); - try { - Method[] methods = classObj.getDeclaredMethods(); - Optional functionalBeanMethods = KafkaStreamsBinderUtils.findMethodWithName(key, methods); - if (functionalBeanMethods.isEmpty()) { - methods = classObj.getMethods(); // check the inherited methods - functionalBeanMethods = KafkaStreamsBinderUtils.findMethodWithName(key, methods); - } - if (functionalBeanMethods.isEmpty()) { - final BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(key); - final String factoryMethodName = beanDefinition.getFactoryMethodName(); - functionalBeanMethods = KafkaStreamsBinderUtils.findMethodWithName(factoryMethodName, methods); - } - - if (functionalBeanMethods.isPresent()) { - Method method = functionalBeanMethods.get(); - ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, classObj); - final Class rawClass = resolvableType.getGeneric(0).getRawClass(); - if (rawClass == KStream.class || rawClass == KTable.class || rawClass == GlobalKTable.class) { - if (onlySingleFunction) { - resolvableTypeMap.put(key, resolvableType); - } - else { - discoverOnlyKafkaStreamsResolvableTypes(key, resolvableType); - } - } - } - else { - Optional componentBeanMethods = Arrays.stream(methods) - .filter(m -> m.getName().equals("apply") && isKafkaStreamsTypeFound(m) || - m.getName().equals("accept") && isKafkaStreamsTypeFound(m)).findFirst(); - if (componentBeanMethods.isPresent()) { - Method method = componentBeanMethods.get(); - final ResolvableType resolvableType = ResolvableType.forMethodParameter(method, 0); - final Class rawClass = resolvableType.getRawClass(); - if (rawClass == KStream.class || rawClass == KTable.class || rawClass == GlobalKTable.class) { - if (onlySingleFunction) { - resolvableTypeMap.put(key, resolvableType); - this.methods.put(key, method); - } - else { - discoverOnlyKafkaStreamsResolvableTypesAndMethods(key, resolvableType, method); - } - } - } - } - } - catch (Exception e) { - LOG.error("Function activation issues while mapping the function: " + key, e); - } - } - - private void addResolvableTypeInfo(String key, ResolvableType resolvableType) { - if (kafkaStreamsOnlyResolvableTypes.size() == 1) { - resolvableTypeMap.put(key, resolvableType); - } - else { - final String definition = streamFunctionProperties.getDefinition(); - if (definition == null) { - throw new IllegalStateException("Multiple functions found, but function definition property is not set."); - } - else if (definition.contains(key)) { - resolvableTypeMap.put(key, resolvableType); - } - } - } - - private void discoverOnlyKafkaStreamsResolvableTypes(String key, ResolvableType resolvableType) { - kafkaStreamsOnlyResolvableTypes.put(key, resolvableType); - } - - private void discoverOnlyKafkaStreamsResolvableTypesAndMethods(String key, ResolvableType resolvableType, Method method) { - kafkaStreamsOnlyResolvableTypes.put(key, resolvableType); - kafakStreamsOnlyMethods.put(key, method); - } - - private void addResolvableTypeInfo(String key, Method method) { - if (kafakStreamsOnlyMethods.size() == 1) { - this.methods.put(key, method); - } - else { - final String definition = streamFunctionProperties.getDefinition(); - if (definition == null) { - throw new IllegalStateException("Multiple functions found, but function definition property is not set."); - } - else if (definition.contains(key)) { - this.methods.put(key, method); - } - } - } - - private boolean isKafkaStreamsTypeFound(Method method) { - return KStream.class.isAssignableFrom(method.getParameters()[0].getType()) || - KTable.class.isAssignableFrom(method.getParameters()[0].getType()) || - GlobalKTable.class.isAssignableFrom(method.getParameters()[0].getType()); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionProcessorInvoker.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionProcessorInvoker.java deleted file mode 100644 index ba9333702..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionProcessorInvoker.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.function; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; - -import jakarta.annotation.PostConstruct; - -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderUtils; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsFunctionProcessor; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.core.ResolvableType; - -/** - * @author Soby Chacko - * @author James Forward - * - * @since 2.1.0 - */ -public class KafkaStreamsFunctionProcessorInvoker { - - private final KafkaStreamsFunctionProcessor kafkaStreamsFunctionProcessor; - private final Map resolvableTypeMap; - private final KafkaStreamsBindableProxyFactory[] kafkaStreamsBindableProxyFactories; - private final Map methods; - private final StreamFunctionProperties streamFunctionProperties; - - public KafkaStreamsFunctionProcessorInvoker(Map resolvableTypeMap, - KafkaStreamsFunctionProcessor kafkaStreamsFunctionProcessor, - KafkaStreamsBindableProxyFactory[] kafkaStreamsBindableProxyFactories, - Map methods, StreamFunctionProperties streamFunctionProperties) { - this.kafkaStreamsFunctionProcessor = kafkaStreamsFunctionProcessor; - this.resolvableTypeMap = resolvableTypeMap; - this.kafkaStreamsBindableProxyFactories = kafkaStreamsBindableProxyFactories; - this.methods = methods; - this.streamFunctionProperties = streamFunctionProperties; - } - - @PostConstruct - void invoke() { - final String definition = streamFunctionProperties.getDefinition(); - final String[] functionUnits = KafkaStreamsBinderUtils.deriveFunctionUnits(definition); - - if (functionUnits.length == 0) { - resolvableTypeMap.forEach((key, value) -> { - Optional proxyFactory = - Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p -> p.getFunctionName().equals(key)).findFirst(); - this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(value, key, proxyFactory.get(), methods.get(key), null); - }); - } - - for (String functionUnit : functionUnits) { - if (functionUnit.contains("|")) { - final String[] composedFunctions = functionUnit.split("\\|"); - String[] derivedNameFromComposed = new String[]{""}; - for (String split : composedFunctions) { - derivedNameFromComposed[0] = derivedNameFromComposed[0].concat(split); - } - Optional proxyFactory = - Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p -> p.getFunctionName().equals(derivedNameFromComposed[0])).findFirst(); - proxyFactory.ifPresent(kafkaStreamsBindableProxyFactory -> - this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(resolvableTypeMap.get(composedFunctions[0]), - derivedNameFromComposed[0], kafkaStreamsBindableProxyFactory, methods.get(derivedNameFromComposed[0]), resolvableTypeMap.get(composedFunctions[composedFunctions.length - 1]), composedFunctions)); - } - else { - Optional proxyFactory = - Arrays.stream(kafkaStreamsBindableProxyFactories).filter(p -> p.getFunctionName().equals(functionUnit)).findFirst(); - proxyFactory.ifPresent(kafkaStreamsBindableProxyFactory -> - this.kafkaStreamsFunctionProcessor.setupFunctionInvokerForKafkaStreams(resolvableTypeMap.get(functionUnit), functionUnit, - kafkaStreamsBindableProxyFactory, methods.get(functionUnit), null)); - } - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsBinderConfigurationProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsBinderConfigurationProperties.java deleted file mode 100644 index 95b1c93bc..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsBinderConfigurationProperties.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams.properties; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.streams.DeserializationExceptionHandler; - -/** - * Kafka Streams binder configuration properties. - * - * @author Soby Chacko - * @author Gary Russell - */ -public class KafkaStreamsBinderConfigurationProperties - extends KafkaBinderConfigurationProperties { - - public KafkaStreamsBinderConfigurationProperties(KafkaProperties kafkaProperties) { - super(kafkaProperties); - } - - /** - * Enumeration for various Serde errors. - * - * @deprecated in favor of {@link DeserializationExceptionHandler}. - */ - @Deprecated - public enum SerdeError { - - /** - * Deserialization error handler with log and continue. - */ - logAndContinue, - /** - * Deserialization error handler with log and fail. - */ - logAndFail, - /** - * Deserialization error handler with DLQ send. - */ - sendToDlq - - } - - private String applicationId; - - private StateStoreRetry stateStoreRetry = new StateStoreRetry(); - - private Map functions = new HashMap<>(); - - private KafkaStreamsBinderConfigurationProperties.SerdeError serdeError; - - /** - * {@link org.apache.kafka.streams.errors.DeserializationExceptionHandler} to use when - * there is a deserialization exception. This handler will be applied against all input bindings - * unless overridden at the consumer binding. - */ - private DeserializationExceptionHandler deserializationExceptionHandler; - - private boolean includeStoppedProcessorsForHealthCheck; - - public Map getFunctions() { - return functions; - } - - public void setFunctions(Map functions) { - this.functions = functions; - } - - public StateStoreRetry getStateStoreRetry() { - return stateStoreRetry; - } - - public void setStateStoreRetry(StateStoreRetry stateStoreRetry) { - this.stateStoreRetry = stateStoreRetry; - } - - public String getApplicationId() { - return this.applicationId; - } - - public void setApplicationId(String applicationId) { - this.applicationId = applicationId; - } - - @Deprecated - public KafkaStreamsBinderConfigurationProperties.SerdeError getSerdeError() { - return this.serdeError; - } - - @Deprecated - public void setSerdeError( - KafkaStreamsBinderConfigurationProperties.SerdeError serdeError) { - this.serdeError = serdeError; - if (serdeError == SerdeError.logAndContinue) { - this.deserializationExceptionHandler = DeserializationExceptionHandler.logAndContinue; - } - else if (serdeError == SerdeError.logAndFail) { - this.deserializationExceptionHandler = DeserializationExceptionHandler.logAndFail; - } - else if (serdeError == SerdeError.sendToDlq) { - this.deserializationExceptionHandler = DeserializationExceptionHandler.sendToDlq; - } - } - - public DeserializationExceptionHandler getDeserializationExceptionHandler() { - return deserializationExceptionHandler; - } - - public void setDeserializationExceptionHandler(DeserializationExceptionHandler deserializationExceptionHandler) { - this.deserializationExceptionHandler = deserializationExceptionHandler; - } - - public boolean isIncludeStoppedProcessorsForHealthCheck() { - return includeStoppedProcessorsForHealthCheck; - } - - public void setIncludeStoppedProcessorsForHealthCheck(boolean includeStoppedProcessorsForHealthCheck) { - this.includeStoppedProcessorsForHealthCheck = includeStoppedProcessorsForHealthCheck; - } - - public static class StateStoreRetry { - - private int maxAttempts = 1; - - private long backoffPeriod = 1000; - - public int getMaxAttempts() { - return maxAttempts; - } - - public void setMaxAttempts(int maxAttempts) { - this.maxAttempts = maxAttempts; - } - - public long getBackoffPeriod() { - return backoffPeriod; - } - - public void setBackoffPeriod(long backoffPeriod) { - this.backoffPeriod = backoffPeriod; - } - } - - public static class Functions { - - /** - * Function specific application id. - */ - private String applicationId; - - /** - * Funcion specific configuraiton to use. - */ - private Map configuration; - - public String getApplicationId() { - return applicationId; - } - - public void setApplicationId(String applicationId) { - this.applicationId = applicationId; - } - - public Map getConfiguration() { - return configuration; - } - - public void setConfiguration(Map configuration) { - this.configuration = configuration; - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsBindingProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsBindingProperties.java deleted file mode 100644 index dd61732ca..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsBindingProperties.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams.properties; - -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * Extended binding properties holder that delegates to Kafka Streams producer and - * consumer properties. - * - * @author Marius Bogoevici - */ -public class KafkaStreamsBindingProperties implements BinderSpecificPropertiesProvider { - - private KafkaStreamsConsumerProperties consumer = new KafkaStreamsConsumerProperties(); - - private KafkaStreamsProducerProperties producer = new KafkaStreamsProducerProperties(); - - public KafkaStreamsConsumerProperties getConsumer() { - return this.consumer; - } - - public void setConsumer(KafkaStreamsConsumerProperties consumer) { - this.consumer = consumer; - } - - public KafkaStreamsProducerProperties getProducer() { - return this.producer; - } - - public void setProducer(KafkaStreamsProducerProperties producer) { - this.producer = producer; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsConsumerProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsConsumerProperties.java deleted file mode 100644 index 616fb207f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsConsumerProperties.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.properties; - -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.DeserializationExceptionHandler; - -/** - * Extended properties for Kafka Streams consumer. - * - * @author Marius Bogoevici - * @author Soby Chacko - */ -public class KafkaStreamsConsumerProperties extends KafkaConsumerProperties { - - private String applicationId; - - /** - * Key serde specified per binding. - */ - private String keySerde; - - /** - * Value serde specified per binding. - */ - private String valueSerde; - - /** - * Materialized as a KeyValueStore. - */ - private String materializedAs; - - /** - * Per input binding deserialization handler. - */ - private DeserializationExceptionHandler deserializationExceptionHandler; - - /** - * {@link org.apache.kafka.streams.processor.TimestampExtractor} bean name to use for this consumer. - */ - private String timestampExtractorBeanName; - - /** - * Comma separated list of supported event types for this binding. - */ - private String eventTypes; - - /** - * Record level header key for event type. - * If the default value is overridden, then that is expected on each record header if eventType based - * routing is enabled on this binding (by setting eventTypes). - */ - private String eventTypeHeaderKey = "event_type"; - - /** - * Custom name for the source component from which the processor is consuming from. - */ - private String consumedAs; - - /** - * When event type based routing is enabled, the binder uses the byte[] Serde by default. - * Use this property to override this default behavior by forcing the binder to use the configured or inferred Serde. - */ - private boolean useConfiguredSerdeWhenRoutingEvents; - - public String getApplicationId() { - return this.applicationId; - } - - public void setApplicationId(String applicationId) { - this.applicationId = applicationId; - } - - public String getKeySerde() { - return this.keySerde; - } - - public void setKeySerde(String keySerde) { - this.keySerde = keySerde; - } - - public String getValueSerde() { - return this.valueSerde; - } - - public void setValueSerde(String valueSerde) { - this.valueSerde = valueSerde; - } - - public String getMaterializedAs() { - return this.materializedAs; - } - - public void setMaterializedAs(String materializedAs) { - this.materializedAs = materializedAs; - } - - public String getTimestampExtractorBeanName() { - return timestampExtractorBeanName; - } - - public void setTimestampExtractorBeanName(String timestampExtractorBeanName) { - this.timestampExtractorBeanName = timestampExtractorBeanName; - } - - public DeserializationExceptionHandler getDeserializationExceptionHandler() { - return deserializationExceptionHandler; - } - - public void setDeserializationExceptionHandler(DeserializationExceptionHandler deserializationExceptionHandler) { - this.deserializationExceptionHandler = deserializationExceptionHandler; - } - - public String getEventTypes() { - return eventTypes; - } - - public void setEventTypes(String eventTypes) { - this.eventTypes = eventTypes; - } - - public String getEventTypeHeaderKey() { - return this.eventTypeHeaderKey; - } - - public void setEventTypeHeaderKey(String eventTypeHeaderKey) { - this.eventTypeHeaderKey = eventTypeHeaderKey; - } - - public String getConsumedAs() { - return consumedAs; - } - - public void setConsumedAs(String consumedAs) { - this.consumedAs = consumedAs; - } - - public boolean isUseConfiguredSerdeWhenRoutingEvents() { - return this.useConfiguredSerdeWhenRoutingEvents; - } - - public void setUseConfiguredSerdeWhenRoutingEvents(boolean useConfiguredSerdeWhenRoutingEvents) { - this.useConfiguredSerdeWhenRoutingEvents = useConfiguredSerdeWhenRoutingEvents; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsExtendedBindingProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsExtendedBindingProperties.java deleted file mode 100644 index 4086748b0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsExtendedBindingProperties.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2017-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.properties; - -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * Kafka streams specific extended binding properties class that extends from - * {@link AbstractExtendedBindingProperties}. - * - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -@ConfigurationProperties("spring.cloud.stream.kafka.streams") -public class KafkaStreamsExtendedBindingProperties - // @checkstyle:off - extends - AbstractExtendedBindingProperties { - - // @checkstyle:on - private static final String DEFAULTS_PREFIX = "spring.cloud.stream.kafka.streams.default"; - - @Override - public String getDefaultsPrefix() { - return DEFAULTS_PREFIX; - } - - @Override - public Map getBindings() { - return this.doGetBindings(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return KafkaStreamsBindingProperties.class; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsProducerProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsProducerProperties.java deleted file mode 100644 index 152621c5e..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/properties/KafkaStreamsProducerProperties.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.properties; - -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; - -/** - * Extended properties for Kafka Streams producer. - * - * @author Marius Bogoevici - * @author Soby Chacko - */ -public class KafkaStreamsProducerProperties extends KafkaProducerProperties { - - /** - * Key serde specified per binding. - */ - private String keySerde; - - /** - * Value serde specified per binding. - */ - private String valueSerde; - - /** - * {@link org.apache.kafka.streams.processor.StreamPartitioner} to be used on Kafka Streams producer. - */ - private String streamPartitionerBeanName; - - /** - * Custom name for the sink component to which the processor is producing to. - */ - private String producedAs; - - public String getKeySerde() { - return this.keySerde; - } - - public void setKeySerde(String keySerde) { - this.keySerde = keySerde; - } - - public String getValueSerde() { - return this.valueSerde; - } - - public void setValueSerde(String valueSerde) { - this.valueSerde = valueSerde; - } - - public String getStreamPartitionerBeanName() { - return this.streamPartitionerBeanName; - } - - public void setStreamPartitionerBeanName(String streamPartitionerBeanName) { - this.streamPartitionerBeanName = streamPartitionerBeanName; - } - - public String getProducedAs() { - return producedAs; - } - - public void setProducedAs(String producedAs) { - this.producedAs = producedAs; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/serde/CollectionSerde.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/serde/CollectionSerde.java deleted file mode 100644 index ce217baf7..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/java/org/springframework/cloud/stream/binder/kafka/streams/serde/CollectionSerde.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.serde; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; -import java.util.PriorityQueue; - -import org.apache.kafka.common.serialization.Deserializer; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.serialization.Serializer; - -import org.springframework.kafka.support.serializer.JsonSerde; - -/** - * A convenient {@link Serde} for {@link java.util.Collection} implementations. - * - * Whenever a Kafka Stream application needs to collect data into a container object like - * {@link java.util.Collection}, then this Serde class can be used as a convenience for - * serialization needs. Some examples of where using this may handy is when the application - * needs to do aggregation or reduction operations where it needs to simply hold an - * {@link Iterable} type. - * - * By default, this Serde will use {@link JsonSerde} for serializing the inner objects. - * This can be changed by providing an explicit Serde during creation of this object. - * - * Here is an example of a possible use case: - * - *

- *		.aggregate(ArrayList::new,
- * 					(k, v, aggregates) -> {
- * 							aggregates.add(v);
- * 							return aggregates;
- *                                                },
- * 					Materialized.<String, Collection<Foo>, WindowStore<Bytes, byte[]>>as(
- * 						"foo-store")
- * 						.withKeySerde(Serdes.String())
- *						.withValueSerde(new CollectionSerde<>(Foo.class, ArrayList.class)))
- *  * 
- * - * Supported Collection types by this Serde are - {@link java.util.ArrayList}, {@link java.util.LinkedList}, - * {@link java.util.PriorityQueue} and {@link java.util.HashSet}. Deserializer will throw an exception - * if any other Collection types are used. - * - * @param type of the underlying object that the collection holds - * @author Soby Chacko - * @since 3.0.0 - */ -public class CollectionSerde implements Serde> { - - /** - * Serde used for serializing the inner object. - */ - private final Serde> inner; - - /** - * Type of the collection class. This has to be a class that is - * implementing the {@link java.util.Collection} interface. - */ - private final Class collectionClass; - - /** - * Constructor to use when the application wants to specify the type - * of the Serde used for the inner object. - * - * @param serde specify an explicit Serde - * @param collectionsClass type of the Collection class - */ - public CollectionSerde(Serde serde, Class collectionsClass) { - this.collectionClass = collectionsClass; - this.inner = - Serdes.serdeFrom( - new CollectionSerializer<>(serde.serializer()), - new CollectionDeserializer<>(serde.deserializer(), collectionsClass)); - } - - /** - * Constructor to delegate serialization operations for the inner objects - * to {@link JsonSerde}. - * - * @param targetTypeForJsonSerde target type used by the JsonSerde - * @param collectionsClass type of the Collection class - */ - public CollectionSerde(Class targetTypeForJsonSerde, Class collectionsClass) { - this.collectionClass = collectionsClass; - try (JsonSerde jsonSerde = new JsonSerde(targetTypeForJsonSerde)) { - - this.inner = Serdes.serdeFrom( - new CollectionSerializer<>(jsonSerde.serializer()), - new CollectionDeserializer<>(jsonSerde.deserializer(), collectionsClass)); - } - } - - @Override - public Serializer> serializer() { - return inner.serializer(); - } - - @Override - public Deserializer> deserializer() { - return inner.deserializer(); - } - - @Override - public void configure(Map configs, boolean isKey) { - inner.serializer().configure(configs, isKey); - inner.deserializer().configure(configs, isKey); - } - - @Override - public void close() { - inner.serializer().close(); - inner.deserializer().close(); - } - - private static class CollectionSerializer implements Serializer> { - - - private Serializer inner; - - CollectionSerializer(Serializer inner) { - this.inner = inner; - } - - CollectionSerializer() { } - - @Override - public void configure(Map configs, boolean isKey) { - - } - - @Override - public byte[] serialize(String topic, Collection collection) { - final int size = collection.size(); - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - final DataOutputStream dos = new DataOutputStream(baos); - final Iterator iterator = collection.iterator(); - try { - dos.writeInt(size); - while (iterator.hasNext()) { - final byte[] bytes = inner.serialize(topic, iterator.next()); - dos.writeInt(bytes.length); - dos.write(bytes); - } - } - catch (IOException e) { - throw new RuntimeException("Unable to serialize the provided collection", e); - } - return baos.toByteArray(); - } - - @Override - public void close() { - inner.close(); - } - } - - private static class CollectionDeserializer implements Deserializer> { - private final Deserializer valueDeserializer; - private final Class collectionClass; - - CollectionDeserializer(final Deserializer valueDeserializer, Class collectionClass) { - this.valueDeserializer = valueDeserializer; - this.collectionClass = collectionClass; - } - - @Override - public void configure(Map configs, boolean isKey) { - } - - @Override - public Collection deserialize(String topic, byte[] bytes) { - if (bytes == null || bytes.length == 0) { - return null; - } - - Collection collection = getCollection(); - final DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(bytes)); - - try { - final int records = dataInputStream.readInt(); - for (int i = 0; i < records; i++) { - final byte[] valueBytes = new byte[dataInputStream.readInt()]; - final int read = dataInputStream.read(valueBytes); - if (read != -1) { - collection.add(valueDeserializer.deserialize(topic, valueBytes)); - } - } - } - catch (IOException e) { - throw new RuntimeException("Unable to deserialize collection", e); - } - - return collection; - } - - @Override - public void close() { - } - - private Collection getCollection() { - Collection collection; - if (this.collectionClass.isAssignableFrom(ArrayList.class)) { - collection = new ArrayList<>(); - } - else if (this.collectionClass.isAssignableFrom(HashSet.class)) { - collection = new HashSet<>(); - } - else if (this.collectionClass.isAssignableFrom(LinkedList.class)) { - collection = new LinkedList<>(); - } - else if (this.collectionClass.isAssignableFrom(PriorityQueue.class)) { - collection = new PriorityQueue<>(); - } - else { - throw new IllegalArgumentException("Unsupported collection type - " + this.collectionClass); - } - return collection; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring.binders b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring.binders deleted file mode 100644 index dde457315..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,8 +0,0 @@ -kstream:\ -org.springframework.cloud.stream.binder.kafka.streams.KStreamBinderConfiguration -ktable:\ -org.springframework.cloud.stream.binder.kafka.streams.KTableBinderConfiguration -globalktable:\ -org.springframework.cloud.stream.binder.kafka.streams.GlobalKTableBinderConfiguration - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring.factories b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring.factories deleted file mode 100644 index c13b43f81..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor=\ -org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderEnvironmentPostProcessor diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index ad3040767..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,4 +0,0 @@ -org.springframework.cloud.stream.binder.kafka.streams.ExtendedBindingHandlerMappingsProviderAutoConfiguration -org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderSupportAutoConfiguration -org.springframework.cloud.stream.binder.kafka.streams.function.KafkaStreamsFunctionAutoConfiguration -org.springframework.cloud.stream.binder.kafka.streams.endpoint.KafkaStreamsTopologyEndpointAutoConfiguration diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/EventTypeRoutingWithInferredSerdeTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/EventTypeRoutingWithInferredSerdeTests.java deleted file mode 100644 index 637832588..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/EventTypeRoutingWithInferredSerdeTests.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.header.internals.RecordHeaders; -import org.apache.kafka.common.serialization.IntegerDeserializer; -import org.apache.kafka.common.serialization.IntegerSerializer; -import org.apache.kafka.common.serialization.StringSerializer; -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * For more context around this test, see https://github.com/spring-cloud/spring-cloud-stream/issues/2613. - * - * @author Soby Chacko - */ -@EmbeddedKafka(topics = "foo-2") -public class EventTypeRoutingWithInferredSerdeTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("test-group-1", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put("value.deserializer", IntegerDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "foo-2"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void eventTypeRoutingWorksWhenInferredSerdeThrowsException() { - SpringApplication app = new SpringApplication(EventTypeRoutingWithConfiguredSerdeConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.process-in-0.destination=foo-1", - "--spring.cloud.stream.bindings.process-out-0.destination=foo-2", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.eventTypes=foo,bar", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.useConfiguredSerdeWhenRoutingEvents=true", - "--spring.cloud.stream.kafka.streams.binder.deserializationExceptionHandler=logAndContinue", - "--spring.cloud.stream.kafka.streams.binder.functions.process.applicationId=process-id-foo-0", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put("value.serializer", IntegerSerializer.class); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo-1"); - - Headers headers = new RecordHeaders(); - headers.add(new RecordHeader("event_type", "foo".getBytes())); - - final ProducerRecord producerRecord1 = new ProducerRecord<>("foo-1", 0, 56, 56, headers); - template.send(producerRecord1); - - - final ProducerRecord producerRecord2 = new ProducerRecord<>("foo-1", 0, 57, 57); - template.send(producerRecord2); - - final ProducerRecord producerRecord3 = new ProducerRecord<>("foo-1", 0, 58, 58, headers); - template.send(producerRecord3); - - Headers headers1 = new RecordHeaders(); - headers1.add(new RecordHeader("event_type", "bar".getBytes())); - - final ProducerRecord producerRecord4 = new ProducerRecord<>("foo-1", 0, 59, 59, headers1); - template.send(producerRecord4); - - senderProps.put("value.serializer", StringSerializer.class); - DefaultKafkaProducerFactory pf1 = new DefaultKafkaProducerFactory<>(senderProps); - KafkaTemplate template1 = new KafkaTemplate<>(pf1, true); - template1.setDefaultTopic("foo-1"); - final ProducerRecord producerRecordBar = new ProducerRecord<>("foo-1", 0, 67, "foobar", headers); - // This send should throw a deserializer exception by the Kafka Streams processor. - // Since the processor is provisioned with an exception handler of logAndContinue, it shouldn't affect anything. - template1.send(producerRecordBar); - - final ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - - assertThat(records.count()).isEqualTo(3); - - List keys = new ArrayList<>(); - List values = new ArrayList<>(); - - records.forEach(integerFooConsumerRecord -> { - keys.add(integerFooConsumerRecord.key()); - values.add(integerFooConsumerRecord.value()); - }); - - assertThat(keys).containsExactlyInAnyOrder(56, 58, 59); - assertThat(values).containsExactlyInAnyOrder(56, 58, 59); - } - finally { - pf.destroy(); - } - } - } - - @EnableAutoConfiguration - public static class EventTypeRoutingWithConfiguredSerdeConfig { - - @Bean - public Function, KStream> process() { - return input -> input; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/ExtendedBindingHandlerMappingsProviderAutoConfigurationTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/ExtendedBindingHandlerMappingsProviderAutoConfigurationTests.java deleted file mode 100644 index fdfbd2953..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/ExtendedBindingHandlerMappingsProviderAutoConfigurationTests.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ExtendedBindingHandlerMappingsProviderAutoConfiguration}. - */ -class ExtendedBindingHandlerMappingsProviderAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(KafkaStreamsTestApp.class) - .withPropertyValues( - "spring.cloud.function.ineligible-definitions: sendToDlqAndContinue", - "spring.cloud.stream.kafka.streams.default.consumer.application-id: testApp123", - "spring.cloud.stream.kafka.streams.default.consumer.consumed-as: default-consumer", - "spring.cloud.stream.kafka.streams.default.consumer.materialized-as: default-materializer", - "spring.cloud.stream.kafka.streams.default.producer.produced-as: default-producer", - "spring.cloud.stream.kafka.streams.default.producer.key-serde: default-foo"); - - @Test - void defaultsUsedWhenNoCustomBindingProperties() { - this.contextRunner.run((context) -> { - assertThat(context) - .hasNotFailed() - .hasSingleBean(KafkaStreamsExtendedBindingProperties.class); - KafkaStreamsExtendedBindingProperties extendedBindingProperties = context.getBean(KafkaStreamsExtendedBindingProperties.class); - assertThat(extendedBindingProperties.getExtendedConsumerProperties("process-in-0")) - .hasFieldOrPropertyWithValue("applicationId", "testApp123") - .hasFieldOrPropertyWithValue("consumedAs", "default-consumer") - .hasFieldOrPropertyWithValue("materializedAs", "default-materializer"); - assertThat(extendedBindingProperties.getExtendedProducerProperties("process-out-0")) - .hasFieldOrPropertyWithValue("producedAs", "default-producer") - .hasFieldOrPropertyWithValue("keySerde", "default-foo"); - }); - } - - @Test - void defaultsRespectedWhenCustomBindingProperties() { - this.contextRunner - .withPropertyValues( - "spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.consumed-as: custom-consumer", - "spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.produced-as: custom-producer") - .run((context) -> { - assertThat(context) - .hasNotFailed() - .hasSingleBean(KafkaStreamsExtendedBindingProperties.class); - KafkaStreamsExtendedBindingProperties extendedBindingProperties = context.getBean(KafkaStreamsExtendedBindingProperties.class); - assertThat(extendedBindingProperties.getExtendedConsumerProperties("process-in-0")) - .hasFieldOrPropertyWithValue("applicationId", "testApp123") - .hasFieldOrPropertyWithValue("consumedAs", "custom-consumer") - .hasFieldOrPropertyWithValue("materializedAs", "default-materializer"); - assertThat(extendedBindingProperties.getExtendedProducerProperties("process-out-0")) - .hasFieldOrPropertyWithValue("producedAs", "custom-producer") - .hasFieldOrPropertyWithValue("keySerde", "default-foo"); - }); - } - - @EnableAutoConfiguration - static class KafkaStreamsTestApp { - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/InteractiveQueryServiceMultiStateStoreTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/InteractiveQueryServiceMultiStateStoreTests.java deleted file mode 100644 index 30363d030..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/InteractiveQueryServiceMultiStateStoreTests.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import java.time.Duration; -import java.util.function.Consumer; - -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StoreQueryParameters; -import org.apache.kafka.streams.errors.UnknownStateStoreException; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.ValueTransformerWithKey; -import org.apache.kafka.streams.processor.ProcessorContext; -import org.apache.kafka.streams.state.KeyValueStore; -import org.apache.kafka.streams.state.QueryableStoreTypes; -import org.apache.kafka.streams.state.StoreBuilder; -import org.apache.kafka.streams.state.Stores; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests for the {@link InteractiveQueryService} when dealing with multiple KafkaStreams apps and state stores. - * - * @author Chris Bono - * @author Soby Chacko - */ -@EmbeddedKafka(topics = {"input1", "input2"}) -class InteractiveQueryServiceMultiStateStoreTests { - - private static final String STORE_1_NAME = "store1"; - private static final String STORE_2_NAME = "store2"; - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void stateStoreAvailableOnProperAppWhenAppServerPropertySet() { - stateStoreAvailableOnProperApp(true); - } - - @Test - void stateStoreAvailableOnProperAppWhenAppServerPropertyNotSet() { - stateStoreAvailableOnProperApp(false); - } - - private void stateStoreAvailableOnProperApp(boolean shouldSetAppServerProperty) { - String appServerArg = shouldSetAppServerProperty ? - "--spring.cloud.stream.kafka.streams.binder.configuration.application.server=" + embeddedKafka.getBrokersAsString() : - "--foo=bar"; - try (ConfigurableApplicationContext context = new SpringApplicationBuilder() - .sources(MultipleAppsWithUsedStateStoresTestApplication.class) - .web(WebApplicationType.NONE) - .run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=app1;app2", - "--spring.cloud.stream.function.bindings.app1-in-0=input1", - "--spring.cloud.stream.function.bindings.app2-in-0=input2", - "--spring.cloud.stream.kafka.streams.binder.functions.app1.application-id=stateStoreTestApp1", - "--spring.cloud.stream.kafka.streams.binder.functions.app2.application-id=stateStoreTestApp2", - appServerArg, - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString()) - ) { - waitForRunningStreams(context.getBean(KafkaStreamsRegistry.class)); - - InteractiveQueryService queryService = context.getBean(InteractiveQueryService.class); - - // The KafkaStreams.store() used by query service is non-deterministic so perform the operation multiple times to - // surface any possible issues. Also, no need to actually write anything to the stores, the store.get() call will - // cause a failure when the state store is invalid. - for (int i = 0; i < 100; i++) { - assertThat(queryService.getQueryableStore(STORE_1_NAME, QueryableStoreTypes.keyValueStore()) - .get("someKey")).isNull(); - assertThat(queryService.getQueryableStore(STORE_2_NAME, QueryableStoreTypes.keyValueStore()) - .get("someKey")).isNull(); - } - } - } - - @Test - void stateStoreNotAvailableThrowsException() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder() - .sources(MultipleAppsWithUnusedStateStoresTestApplication.class) - .web(WebApplicationType.NONE) - .run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=app1;app2", - "--spring.cloud.stream.function.bindings.app1-in-0=input1", - "--spring.cloud.stream.function.bindings.app2-in-0=input2", - "--spring.cloud.stream.kafka.streams.binder.functions.app1.application-id=stateStoreTestApp3", - "--spring.cloud.stream.kafka.streams.binder.functions.app2.application-id=stateStoreTestApp4", - "--spring.cloud.stream.kafka.streams.binder.configuration.application.server=" - + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()) - ) { - waitForRunningStreams(context.getBean(KafkaStreamsRegistry.class)); - - InteractiveQueryService queryService = context.getBean(InteractiveQueryService.class); - - assertThatThrownBy(() -> queryService.getQueryableStore(STORE_1_NAME, QueryableStoreTypes.keyValueStore())) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Error retrieving state store: " + STORE_1_NAME) - .hasRootCauseInstanceOf(UnknownStateStoreException.class) - .hasRootCauseMessage("Store (" + STORE_1_NAME + ") not available to Streams instance"); - assertThatThrownBy(() -> queryService.getQueryableStore(STORE_2_NAME, QueryableStoreTypes.keyValueStore())) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Error retrieving state store: " + STORE_2_NAME) - .hasRootCauseInstanceOf(UnknownStateStoreException.class) - .hasRootCauseMessage("Store (" + STORE_2_NAME + ") not available to Streams instance"); - } - } - - @Test - @SuppressWarnings("unchecked") - void storeQueryParameterCustomizerIsApplied() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder() - .sources(StoreQueryParameterCustomizerTestApplication.class) - .web(WebApplicationType.NONE) - .run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=app1", - "--spring.cloud.stream.function.bindings.app1-in-0=input1", - "--spring.cloud.stream.kafka.streams.binder.functions.app1.application-id=storeQueryParameterCustomizerIsAppliedAppId", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString()) - ) { - waitForRunningStreams(context.getBean(KafkaStreamsRegistry.class)); - - InteractiveQueryService queryService = context.getBean(InteractiveQueryService.class); - - StoreQueryParametersCustomizer storeQueryParametersCustomizer = context.getBean(StoreQueryParametersCustomizer.class); - StoreQueryParameters storeQueryParams = StoreQueryParameters.fromNameAndType(STORE_1_NAME, QueryableStoreTypes.keyValueStore()); - - when(storeQueryParametersCustomizer.customize(Mockito.any(StoreQueryParameters.class))).thenReturn(storeQueryParams); - - queryService.getQueryableStore(STORE_1_NAME, QueryableStoreTypes.keyValueStore()).get("someKey"); - verify(storeQueryParametersCustomizer).customize(Mockito.any(StoreQueryParameters.class)); - } - } - - private void waitForRunningStreams(KafkaStreamsRegistry registry) { - await().atMost(Duration.ofSeconds(60)) - .until(() -> registry.streamsBuilderFactoryBeans().stream() - .allMatch(x -> x.getKafkaStreams().state().equals(KafkaStreams.State.RUNNING))); - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - static class MultipleAppsWithUsedStateStoresTestApplication { - - private static final Logger log = LoggerFactory.getLogger(MultipleAppsWithUsedStateStoresTestApplication.class); - - public static void main(String[] args) { - SpringApplication.run(MultipleAppsWithUsedStateStoresTestApplication.class, args); - } - - @Bean - public StoreBuilder> store1() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore(STORE_1_NAME), Serdes.String(), Serdes.String()); - } - - @Bean - public Consumer> app1() { - return s -> s - .transformValues(EchoTransformer::new, STORE_1_NAME) - .foreach((k, v) -> log.info("Echo {} -> {} into {}", k, v, STORE_1_NAME)); - } - - @Bean - public StoreBuilder> store2() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore(STORE_2_NAME), Serdes.String(), Serdes.String()); - } - - @Bean - public Consumer> app2() { - return s -> s - .transformValues(EchoTransformer::new, STORE_2_NAME) - .foreach((k, v) -> log.info("Echo {} -> {} into {}", k, v, STORE_2_NAME)); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - static class MultipleAppsWithUnusedStateStoresTestApplication { - - private static final Logger log = LoggerFactory.getLogger(MultipleAppsWithUnusedStateStoresTestApplication.class); - - public static void main(String[] args) { - SpringApplication.run(MultipleAppsWithUnusedStateStoresTestApplication.class, args); - } - - @Bean - public StoreBuilder> store1() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore(STORE_1_NAME), Serdes.String(), Serdes.String()); - } - - @Bean - public Consumer> app1() { - // NOTE: No reference to the state store via transformer - return s -> s - .foreach((k, v) -> log.info("Echo {} -> {} into {}", k, v, STORE_1_NAME)); - } - - @Bean - public StoreBuilder> store2() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore(STORE_2_NAME), Serdes.String(), Serdes.String()); - } - - @Bean - public Consumer> app2() { - // NOTE: No reference to the state store via transformer - return s -> s - .foreach((k, v) -> log.info("Echo {} -> {} into {}", k, v, STORE_2_NAME)); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - static class StoreQueryParameterCustomizerTestApplication { - - private static final Logger log = LoggerFactory.getLogger(StoreQueryParameterCustomizerTestApplication.class); - - - @Bean - public StoreBuilder> store1() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore(STORE_1_NAME), Serdes.String(), Serdes.String()); - } - - @Bean - public Consumer> app1() { - return s -> s - .transformValues(EchoTransformer::new, STORE_1_NAME) - .foreach((k, v) -> log.info("Echo {} -> {} into {}", k, v, STORE_1_NAME)); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - - @Bean - StoreQueryParametersCustomizer storeQueryParametersCustomizer() { - return Mockito.mock(StoreQueryParametersCustomizer.class); - } - } - - static class EchoTransformer implements ValueTransformerWithKey { - - @Override - public void init(ProcessorContext context) { - } - - @Override - public String transform(String key, String value) { - return value; - } - - @Override - public void close() { - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderEnvironmentPostProcessorTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderEnvironmentPostProcessorTests.java deleted file mode 100644 index dabdf26b9..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderEnvironmentPostProcessorTests.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.streams; - -import java.util.function.Consumer; - -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -/** - * Tests for {@link KafkaStreamsBinderEnvironmentPostProcessor}. - * - * @author Chris Bono - */ -@EmbeddedKafka -public class KafkaStreamsBinderEnvironmentPostProcessorTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void defaultIneligibleFunctionIsSet() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(KafkaStreamsBinderEnvironmentPostProcessorTests.SimpleKafkaStreamsApplication.class) - .web(WebApplicationType.NONE).run( - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString())) { - assertThat(context.getEnvironment().getProperty("spring.cloud.function.ineligible-definitions")) - .isEqualTo("sendToDlqAndContinue"); - } - } - - @Test - void userSpecifiedIneligibleFunctionIsAppendedToDefaultList() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(KafkaStreamsBinderEnvironmentPostProcessorTests.SimpleKafkaStreamsApplication.class) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.ineligible-definitions=foo", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString())) { - assertThat(context.getEnvironment().getProperty("spring.cloud.function.ineligible-definitions")) - .isEqualTo("sendToDlqAndContinue,foo"); - } - } - - @EnableAutoConfiguration - static class SimpleKafkaStreamsApplication { - @Bean - public Consumer> logInput() { - return s -> { - // No-op consumer - }; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderUtilsTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderUtilsTests.java deleted file mode 100644 index 0a1ee3fc8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsBinderUtilsTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link KafkaStreamsBinderUtils} functions. - * - * @author James Forward - * @author Soby Chacko - */ -class KafkaStreamsBinderUtilsTests { - - @Test - void testDeriveFunctionUnitsWithVariedSpacings() { - final String definition = "firstFunction; secondFunction; thirdFunction;fourthFunction"; - - final String[] functionUnits = KafkaStreamsBinderUtils.deriveFunctionUnits(definition); - - assertThat(functionUnits.length).isEqualTo(4); - assertThat(functionUnits[0]).isEqualTo("firstFunction"); - assertThat(functionUnits[1]).isEqualTo("secondFunction"); - assertThat(functionUnits[2]).isEqualTo("thirdFunction"); - assertThat(functionUnits[3]).isEqualTo("fourthFunction"); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsEventTypeRoutingTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsEventTypeRoutingTests.java deleted file mode 100644 index 128a4b303..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsEventTypeRoutingTests.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.header.internals.RecordHeaders; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.serializer.JsonDeserializer; -import org.springframework.kafka.support.serializer.JsonSerializer; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.util.Assert; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka(topics = "foo-2") -public class KafkaStreamsEventTypeRoutingTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - private static CountDownLatch LATCH = new CountDownLatch(3); - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("test-group-1", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put("value.deserializer", JsonDeserializer.class); - consumerProps.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "foo-2"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - //See https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1003 for more context on this test. - @Test - void testRoutingWorksBasedOnEventTypes() { - SpringApplication app = new SpringApplication(EventTypeRoutingTestConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.process-in-0.destination=foo-1", - "--spring.cloud.stream.bindings.process-out-0.destination=foo-2", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.eventTypes=foo,bar", - "--spring.cloud.stream.kafka.streams.binder.functions.process.applicationId=process-id-foo-0", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put("value.serializer", JsonSerializer.class); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo-1"); - Foo foo1 = new Foo(); - foo1.setFoo("foo-1"); - Headers headers = new RecordHeaders(); - headers.add(new RecordHeader("event_type", "foo".getBytes())); - - final ProducerRecord producerRecord1 = new ProducerRecord<>("foo-1", 0, 56, foo1, headers); - template.send(producerRecord1); - - Foo foo2 = new Foo(); - foo2.setFoo("foo-2"); - - final ProducerRecord producerRecord2 = new ProducerRecord<>("foo-1", 0, 57, foo2); - template.send(producerRecord2); - - Foo foo3 = new Foo(); - foo3.setFoo("foo-3"); - - final ProducerRecord producerRecord3 = new ProducerRecord<>("foo-1", 0, 58, foo3, headers); - template.send(producerRecord3); - - Foo foo4 = new Foo(); - foo4.setFoo("foo-4"); - Headers headers1 = new RecordHeaders(); - headers1.add(new RecordHeader("event_type", "bar".getBytes())); - - final ProducerRecord producerRecord4 = new ProducerRecord<>("foo-1", 0, 59, foo4, headers1); - template.send(producerRecord4); - - final ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - - assertThat(records.count()).isEqualTo(3); - - List keys = new ArrayList<>(); - List values = new ArrayList<>(); - - records.forEach(integerFooConsumerRecord -> { - keys.add(integerFooConsumerRecord.key()); - values.add(integerFooConsumerRecord.value()); - }); - - assertThat(keys).containsExactlyInAnyOrder(56, 58, 59); - assertThat(values).containsExactlyInAnyOrder(foo1, foo3, foo4); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testRoutingWorksBasedOnEventTypesConsumer() throws Exception { - SpringApplication app = new SpringApplication(EventTypeRoutingTestConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consumer", - "--spring.cloud.stream.bindings.consumer-in-0.destination=foo-consumer-1", - "--spring.cloud.stream.kafka.streams.bindings.consumer-in-0.consumer.eventTypes=foo,bar", - "--spring.cloud.stream.kafka.streams.binder.functions.consumer.applicationId=consumer-id-foo-0", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put("value.serializer", JsonSerializer.class); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo-consumer-1"); - Foo foo1 = new Foo(); - foo1.setFoo("foo-1"); - Headers headers = new RecordHeaders(); - headers.add(new RecordHeader("event_type", "foo".getBytes())); - - final ProducerRecord producerRecord1 = new ProducerRecord<>("foo-consumer-1", 0, 56, foo1, headers); - template.send(producerRecord1); - - Foo foo2 = new Foo(); - foo2.setFoo("foo-2"); - - final ProducerRecord producerRecord2 = new ProducerRecord<>("foo-consumer-1", 0, 57, foo2); - template.send(producerRecord2); - - Foo foo3 = new Foo(); - foo3.setFoo("foo-3"); - - final ProducerRecord producerRecord3 = new ProducerRecord<>("foo-consumer-1", 0, 58, foo3, headers); - template.send(producerRecord3); - - Foo foo4 = new Foo(); - foo4.setFoo("foo-4"); - Headers headers1 = new RecordHeaders(); - headers1.add(new RecordHeader("event_type", "bar".getBytes())); - - final ProducerRecord producerRecord4 = new ProducerRecord<>("foo-consumer-1", 0, 59, foo4, headers1); - template.send(producerRecord4); - - Assert.isTrue(LATCH.await(10, TimeUnit.SECONDS), "Foo"); - } - finally { - pf.destroy(); - } - } - } - - @EnableAutoConfiguration - public static class EventTypeRoutingTestConfig { - - @Bean - public Function, KStream> process() { - return input -> input; - } - - @Bean - public java.util.function.Consumer> consumer() { - return ktable -> ktable.toStream().foreach((key, value) -> { - LATCH.countDown(); - }); - } - - @Bean - public java.util.function.Consumer> global() { - return ktable -> { - }; - } - - } - - static class Foo { - String foo; - - public String getFoo() { - return foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Foo foo1 = (Foo) o; - return Objects.equals(foo, foo1.foo); - } - - @Override - public int hashCode() { - return Objects.hash(foo); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsFunctionCompositionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsFunctionCompositionTests.java deleted file mode 100644 index e5d8094fa..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsFunctionCompositionTests.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright 2021-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.cloud.stream.binder.kafka.streams; - -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; -import org.apache.kafka.streams.kstream.ForeachAction; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.util.Assert; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka(topics = {"fooFuncanotherFooFunc-out-0", "bar"}) -public class KafkaStreamsFunctionCompositionTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - private static final CountDownLatch countDownLatch1 = new CountDownLatch(1); - private static final CountDownLatch countDownLatch2 = new CountDownLatch(1); - private static final CountDownLatch countDownLatch3 = new CountDownLatch(2); - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("fn-composition-group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "fooFuncanotherFooFunc-out-0", "bar"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testBasicFunctionCompositionWithDefaultDestination() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig1.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=fooFunc|anotherFooFunc;anotherProcess", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("fooFuncanotherFooFunc-in-0"); - template.sendDefault("foobar!!"); - //Verify non-composed funcions can be run standalone with composed function chains, i.e foo|bar;buzz - template.setDefaultTopic("anotherProcess-in-0"); - template.sendDefault("this is crazy!!!"); - Thread.sleep(1000); - - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "fooFuncanotherFooFunc-out-0"); - assertThat(cr.value().contains("foobar!!")).isTrue(); - - Assert.isTrue(countDownLatch1.await(5, TimeUnit.SECONDS), "anotherProcess consumer didn't trigger."); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testBasicFunctionCompositionWithDestinaion() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig1.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=fooFunc|anotherFooFunc;anotherProcess", - "--spring.cloud.stream.bindings.fooFuncanotherFooFunc-in-0.destination=foo", - "--spring.cloud.stream.bindings.fooFuncanotherFooFunc-out-0.destination=bar", - "--spring.cloud.stream.bindings.anotherProcess-in-0.destination=buzz", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo"); - template.sendDefault("foobar!!"); - template.setDefaultTopic("buzz"); - template.sendDefault("this is crazy!!!"); - Thread.sleep(1000); - - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "bar"); - assertThat(cr.value().contains("foobar!!")).isTrue(); - - Assert.isTrue(countDownLatch1.await(5, TimeUnit.SECONDS), "anotherProcess consumer didn't trigger."); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testFunctionToConsumerComposition() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig2.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=fooFunc|anotherProcess", - "--spring.cloud.stream.bindings.fooFuncanotherProcess-in-0.destination=foo", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo"); - template.sendDefault("foobar!!"); - - Thread.sleep(1000); - - Assert.isTrue(countDownLatch2.await(5, TimeUnit.SECONDS), "anotherProcess consumer didn't trigger."); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testBiFunctionToConsumerComposition() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig3.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=fooBiFunc|anotherProcess", - "--spring.cloud.stream.bindings.fooBiFuncanotherProcess-in-0.destination=foo", - "--spring.cloud.stream.bindings.fooBiFuncanotherProcess-in-1.destination=foo-1", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo"); - template.sendDefault("foobar!!"); - - template.setDefaultTopic("foo-1"); - template.sendDefault("another foobar!!"); - - Thread.sleep(1000); - - Assert.isTrue(countDownLatch3.await(5, TimeUnit.SECONDS), "anotherProcess consumer didn't trigger."); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testChainedFunctionsAsComposed() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig4.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.kafka.streams.binder.applicationId=my-app-id", - "--spring.cloud.function.definition=fooBiFunc|anotherFooFunc|yetAnotherFooFunc|lastFunctionInChain", - "--spring.cloud.stream.function.bindings.fooBiFuncanotherFooFuncyetAnotherFooFunclastFunctionInChain-in-0=input1", - "--spring.cloud.stream.function.bindings.fooBiFuncanotherFooFuncyetAnotherFooFunclastFunctionInChain-in-1=input2", - "--spring.cloud.stream.function.bindings.fooBiFuncanotherFooFuncyetAnotherFooFunclastFunctionInChain-out-0=output", - "--spring.cloud.stream.bindings.input1.destination=my-foo-1", - "--spring.cloud.stream.bindings.input2.destination=my-foo-2", - "--spring.cloud.stream.bindings.output.destination=bar", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - - template.setDefaultTopic("my-foo-2"); - template.sendDefault("foo-1", "foo2"); - - template.setDefaultTopic("my-foo-1"); - template.sendDefault("foo-1", "foo1"); - - Thread.sleep(1000); - - final ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - assertThat(records.iterator().hasNext()).isTrue(); - assertThat(records.iterator().next().value().equals("foo1foo2From-anotherFooFuncFrom-yetAnotherFooFuncFrom-lastFunctionInChain")).isTrue(); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testFirstFunctionCurriedThenComposeWithOtherFunctions() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig5.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.kafka.streams.binder.applicationId=my-app-id-xyz", - "--spring.cloud.function.definition=curriedFunc|anotherFooFunc|yetAnotherFooFunc|lastFunctionInChain", - "--spring.cloud.stream.function.bindings.curriedFuncanotherFooFuncyetAnotherFooFunclastFunctionInChain-in-0=input1", - "--spring.cloud.stream.function.bindings.curriedFuncanotherFooFuncyetAnotherFooFunclastFunctionInChain-in-1=input2", - "--spring.cloud.stream.function.bindings.curriedFuncanotherFooFuncyetAnotherFooFunclastFunctionInChain-out-0=output", - "--spring.cloud.stream.bindings.input1.destination=my-foo-1", - "--spring.cloud.stream.bindings.input2.destination=my-foo-2", - "--spring.cloud.stream.bindings.output.destination=bar", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - - template.setDefaultTopic("my-foo-2"); - template.sendDefault("foo-1", "foo2"); - - Thread.sleep(1000); - - template.setDefaultTopic("my-foo-1"); - template.sendDefault("foo-1", "foo1"); - - Thread.sleep(1000); - - final ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - assertThat(records.iterator().hasNext()).isTrue(); - assertThat(records.iterator().next().value().equals("foo1foo2From-anotherFooFuncFrom-yetAnotherFooFuncFrom-lastFunctionInChain")).isTrue(); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testFunctionToConsumerCompositionWithFunctionProducesKTable() throws InterruptedException { - SpringApplication app = new SpringApplication(FunctionCompositionConfig6.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=fooFunc|anotherProcess", - "--spring.cloud.stream.bindings.fooFuncanotherProcess-in-0.destination=foo", - "--spring.cloud.stream.bindings.fooFuncanotherProcess-out-0.destination=bar", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foo"); - template.sendDefault("foo", "foobar!!"); - - Thread.sleep(1000); - - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "bar"); - assertThat(cr.value().contains("foobar!!")).isTrue(); - } - finally { - pf.destroy(); - } - } - } - - @EnableAutoConfiguration - public static class FunctionCompositionConfig1 { - - @Bean - public Function, KStream> fooFunc() { - return input -> input.peek((s, s2) -> { - System.out.println("hello: " + s2); - }); - } - - @Bean - public Function, KStream> anotherFooFunc() { - return input -> input.peek((s, s2) -> System.out.println("hello Foo: " + s2)); - } - - @Bean - public java.util.function.Consumer> anotherProcess() { - return c -> c.foreach((s, s2) -> { - System.out.println("s2s2s2::" + s2); - countDownLatch1.countDown(); - }); - } - } - - @EnableAutoConfiguration - public static class FunctionCompositionConfig2 { - - @Bean - public Function, KStream> fooFunc() { - return input -> input.peek((s, s2) -> { - System.out.println("hello: " + s2); - }); - } - - @Bean - public java.util.function.Consumer> anotherProcess() { - return c -> c.foreach((s, s2) -> { - System.out.println("s2s2s2::" + s2); - countDownLatch2.countDown(); - }); - } - } - - @EnableAutoConfiguration - public static class FunctionCompositionConfig3 { - - @Bean - public BiFunction, KStream, KStream> fooBiFunc() { - return KStream::merge; - } - - @Bean - public java.util.function.Consumer> anotherProcess() { - return c -> c.foreach((s, s2) -> { - System.out.println("s2s2s2::" + s2); - countDownLatch3.countDown(); - }); - } - } - - @EnableAutoConfiguration - public static class FunctionCompositionConfig4 { - - @Bean - public BiFunction, KTable, KStream> fooBiFunc() { - return (a, b) -> a.join(b, (value1, value2) -> value1 + value2); - } - - @Bean - public Function, KStream> anotherFooFunc() { - return input -> input.mapValues(value -> value + "From-anotherFooFunc"); - } - - @Bean - public Function, KStream> yetAnotherFooFunc() { - return input -> input.mapValues(value -> value + "From-yetAnotherFooFunc"); - } - - @Bean - public Function, KStream> lastFunctionInChain() { - return input -> input.mapValues(value -> value + "From-lastFunctionInChain"); - } - } - - @EnableAutoConfiguration - public static class FunctionCompositionConfig5 { - - @Bean - public Function, Function, KStream>> curriedFunc() { - return a -> b -> - a.join(b, (value1, value2) -> value1 + value2); - } - - @Bean - public Function, KStream> anotherFooFunc() { - return input -> input.mapValues(value -> value + "From-anotherFooFunc"); - } - - @Bean - public Function, KStream> yetAnotherFooFunc() { - return input -> input.mapValues(value -> value + "From-yetAnotherFooFunc"); - } - - @Bean - public Function, KStream> lastFunctionInChain() { - return input -> input.mapValues(value -> value + "From-lastFunctionInChain"); - } - } - - @EnableAutoConfiguration - public static class FunctionCompositionConfig6 { - - @Bean - public Function, KTable> fooFunc() { - return ks -> { - ks.foreach(new ForeachAction() { - @Override - public void apply(String key, String value) { - System.out.println(); - } - }); - return ks.toTable(); - }; - } - - @Bean - public Function, KStream> anotherProcess() { - return KTable::toStream; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsInteractiveQueryIntegrationTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsInteractiveQueryIntegrationTests.java deleted file mode 100644 index e28e2f388..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsInteractiveQueryIntegrationTests.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams; - -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.serialization.IntegerSerializer; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.serialization.StringSerializer; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.KeyQueryMetadata; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.StoreQueryParameters; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.state.HostInfo; -import org.apache.kafka.streams.state.QueryableStoreType; -import org.apache.kafka.streams.state.QueryableStoreTypes; -import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBinderConfigurationProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.serializer.JsonSerde; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.internal.verification.VerificationModeFactory.times; - -/** - * @author Soby Chacko - * @author Gary Russell - * @author Nico Pommerening - */ -@EmbeddedKafka(topics = "counts-id") -public class KafkaStreamsInteractiveQueryIntegrationTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() throws Exception { - Map consumerProps = KafkaTestUtils.consumerProps("group-id", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "counts-id"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void stateStoreRetrievalRetriedOnFailure() { - - StreamsBuilderFactoryBean mock = Mockito.mock(StreamsBuilderFactoryBean.class); - KafkaStreams mockKafkaStreams = Mockito.mock(KafkaStreams.class); - Mockito.when(mock.getKafkaStreams()).thenReturn(mockKafkaStreams); - KafkaStreamsRegistry kafkaStreamsRegistry = new KafkaStreamsRegistry(); - kafkaStreamsRegistry.registerKafkaStreams(mock); - Mockito.when(mock.isRunning()).thenReturn(true); - Properties mockProperties = new Properties(); - mockProperties.put(StreamsConfig.APPLICATION_ID_CONFIG, "fooApp"); - Mockito.when(mock.getStreamsConfiguration()).thenReturn(mockProperties); - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties = - new KafkaStreamsBinderConfigurationProperties(new KafkaProperties()); - binderConfigurationProperties.getStateStoreRetry().setMaxAttempts(3); - InteractiveQueryService interactiveQueryService = new InteractiveQueryService(kafkaStreamsRegistry, - binderConfigurationProperties); - - QueryableStoreType> storeType = QueryableStoreTypes.keyValueStore(); - try { - interactiveQueryService.getQueryableStore("foo", storeType); - } - catch (Exception ignored) { - - } - Mockito.verify(mockKafkaStreams, times(3)) - .store(StoreQueryParameters.fromNameAndType("foo", storeType)); - } - - @Test - void hostInfoRetrievalRetriedOnFailure() { - StreamsBuilderFactoryBean mock = Mockito.mock(StreamsBuilderFactoryBean.class); - KafkaStreams mockKafkaStreams = Mockito.mock(KafkaStreams.class); - Mockito.when(mock.getKafkaStreams()).thenReturn(mockKafkaStreams); - KafkaStreamsRegistry kafkaStreamsRegistry = new KafkaStreamsRegistry(); - kafkaStreamsRegistry.registerKafkaStreams(mock); - Mockito.when(mock.isRunning()).thenReturn(true); - Properties mockProperties = new Properties(); - mockProperties.put(StreamsConfig.APPLICATION_ID_CONFIG, "foobarApp-123"); - Mockito.when(mock.getStreamsConfiguration()).thenReturn(mockProperties); - KafkaStreamsBinderConfigurationProperties binderConfigurationProperties = - new KafkaStreamsBinderConfigurationProperties(new KafkaProperties()); - binderConfigurationProperties.getStateStoreRetry().setMaxAttempts(3); - InteractiveQueryService interactiveQueryService = new InteractiveQueryService(kafkaStreamsRegistry, - binderConfigurationProperties); - - QueryableStoreType> storeType = QueryableStoreTypes.keyValueStore(); - final StringSerializer serializer = new StringSerializer(); - try { - interactiveQueryService.getHostInfo("foo", "foobarApp-key", serializer); - } - catch (Exception ignored) { - - } - Mockito.verify(mockKafkaStreams, times(3)) - .queryMetadataForKey("foo", "foobarApp-key", serializer); - } - - @Test - void queryServiceProvidesStateStoreAndHostInfo() { - SpringApplication app = new SpringApplication(ProductCountApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.bindings.input.destination=foos", - "--spring.cloud.stream.bindings.output.destination=counts-id", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId=ProductCountApplication-def", - "--spring.cloud.stream.kafka.streams.binder.configuration.application.server" - + "=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - receiveAndValidateFoo(context); - } - finally { - context.close(); - } - } - - private void receiveAndValidateFoo(ConfigurableApplicationContext context) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foos"); - template.sendDefault("{\"id\":\"123\"}"); - - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "counts-id"); - assertThat(cr.value()).isIn("Count for product with ID 123: 1", "Count for product with ID 123: 2"); - - // Make sure the count can be retrieved from local state store - ProductCountApplication.Foo foo = context.getBean(ProductCountApplication.Foo.class); - assertThat(foo.getProductStock(123)).isIn(1L, 2L); - - // perform assertions on HostInfo related methods in InteractiveQueryService - InteractiveQueryService interactiveQueryService = context - .getBean(InteractiveQueryService.class); - HostInfo currentHostInfo = interactiveQueryService.getCurrentHostInfo(); - - assertThat(currentHostInfo.host() + ":" + currentHostInfo.port()) - .isEqualTo(embeddedKafka.getBrokersAsString()); - - final KeyQueryMetadata keyQueryMetadata = interactiveQueryService.getKeyQueryMetadata("prod-id-count-store", - 123, new IntegerSerializer()); - final HostInfo activeHost = keyQueryMetadata.getActiveHost(); - assertThat(activeHost.host() + ":" + activeHost.port()) - .isEqualTo(embeddedKafka.getBrokersAsString()); - - final KafkaStreams kafkaStreams = interactiveQueryService.getKafkaStreams("prod-id-count-store", - 123, new IntegerSerializer()); - assertThat(kafkaStreams).isNotNull(); - assertThat(interactiveQueryService.getKafkaStreams("non-existent-store", - 123, new IntegerSerializer())).isNull(); - - HostInfo hostInfo = interactiveQueryService.getHostInfo("prod-id-count-store", - 123, new IntegerSerializer()); - assertThat(hostInfo.host() + ":" + hostInfo.port()) - .isEqualTo(embeddedKafka.getBrokersAsString()); - - assertThatThrownBy(() -> interactiveQueryService - .getHostInfo("prod-id-count-store-foo", 123, new IntegerSerializer())) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Error when retrieving state store."); - - final List hostInfos = interactiveQueryService.getAllHostsInfo("prod-id-count-store"); - assertThat(hostInfos.size()).isEqualTo(1); - final HostInfo hostInfo1 = hostInfos.get(0); - assertThat(hostInfo1.host() + ":" + hostInfo1.port()) - .isEqualTo(embeddedKafka.getBrokersAsString()); - } - - @Test - void queryServiceProvidesStateStoreWithoutSettingAppServerProperty() { - SpringApplication app = new SpringApplication(ProductCountApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.bindings.input.destination=foos", - "--spring.cloud.stream.bindings.output.destination=counts-id", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId=ProductCountApplication-def", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString()) - ) { - // Send an input product record - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foos"); - template.sendDefault("{\"id\":\"123\"}"); - - // Wait for output from the streams app - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "counts-id"); - assertThat(cr.value()).isIn("Count for product with ID 123: 1", "Count for product with ID 123: 2"); - - // Make sure the count can be retrieved from local state store - ProductCountApplication.Foo foo = context.getBean(ProductCountApplication.Foo.class); - assertThat(foo.getProductStock(123)).isIn(1L, 2L); - } - } - - @EnableAutoConfiguration - public static class ProductCountApplication { - - @Bean - public Function, KStream> process() { - - return input -> input.filter((key, product) -> product.getId() == 123) - .map((key, value) -> new KeyValue<>(value.id, value)) - .groupByKey(Grouped.with(new Serdes.IntegerSerde(), - new JsonSerde<>(Product.class))) - .count(Materialized.as("prod-id-count-store")).toStream() - .map((key, value) -> new KeyValue<>(null, "Count for product with ID 123: " + value)); - } - - @Bean - public Foo foo(InteractiveQueryService interactiveQueryService) { - return new Foo(interactiveQueryService); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - - static class Foo { - - InteractiveQueryService interactiveQueryService; - - Foo(InteractiveQueryService interactiveQueryService) { - this.interactiveQueryService = interactiveQueryService; - } - - public Long getProductStock(Integer id) { - ReadOnlyKeyValueStore keyValueStore = interactiveQueryService - .getQueryableStore("prod-id-count-store", - QueryableStoreTypes.keyValueStore()); - - return (Long) keyValueStore.get(id); - } - - } - } - - static class Product { - - Integer id; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsVersionAgnosticTopologyInfoFacadeTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsVersionAgnosticTopologyInfoFacadeTests.java deleted file mode 100644 index 0a859d577..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/KafkaStreamsVersionAgnosticTopologyInfoFacadeTests.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.streams; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Properties; - -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.StreamsConfig; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link KafkaStreamsVersionAgnosticTopologyInfoFacade}. - * - * @author Chris Bono - */ -class KafkaStreamsVersionAgnosticTopologyInfoFacadeTests { - - private static final Properties STREAM_PROPS = new Properties(); - - private static final StreamsBuilder STREAM_BUILDER = new StreamsBuilder(); - - static { - STREAM_PROPS.put(StreamsConfig.APPLICATION_ID_CONFIG, "topology-facade-tests-app"); - STREAM_PROPS.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); - STREAM_BUILDER.stream("foo").to("bar"); - } - - @Nested - class KafkaStreams30 { - - @Test - void sourceTopicsForStoreWithTopics() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams30.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams30("topic1"), "store1")).isTrue(); - } - - @Test - void sourceTopicsForStoreWithNoTopics() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams30.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams30(), "store1")).isFalse(); - } - - } - - @Nested - class KafkaStreams31_32 { - - @Test - void sourceTopicsForStoreWithTopics() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams31_32.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams31_32("topic1"), "store1")).isTrue(); - } - - @Test - void sourceTopicsForStoreWithNoTopics() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams31_32.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams31_32(), "store1")).isFalse(); - } - - } - - @Nested - class KafkaStreams33 { - - @Test - void sourceTopicsForStoreWithTopics() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams33.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams33("topic1"), "store1")).isTrue(); - } - - @Test - void sourceTopicsForStoreWithNoTopics() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams33.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams33(), "store1")).isFalse(); - } - } - - @Nested - class NegativeCases { - @Test - void nullTopologyInfo() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreams30.class); - TestToplogyInfoOneArg internalTopologyBuilder = null; - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreams30(internalTopologyBuilder), "store1")).isFalse(); - } - - @Test - void sourceTopicsForStoreMethodNotFound() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreamsNoSourceTopicsMethod.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreamsNoSourceTopicsMethod(), "store1")).isFalse(); - } - - - @Test - void sourceTopicsForStoreThrowsException() { - KafkaStreamsVersionAgnosticTopologyInfoFacade facade = - new KafkaStreamsVersionAgnosticTopologyInfoFacade(TestKafkaStreamsThrowsError.class); - assertThat(facade.streamsAppActuallyHasStore(new TestKafkaStreamsThrowsError(), "store1")).isFalse(); - } - } - - /** - * A test version of KafkaStreams as it exists in kafka-streams 3.0 w/ a topology - * info field named 'internalTopologyBuilder' that in turn has a single-arg version - * of sourceTopicsForStore(String)' that holds the info we need. - */ - static class TestKafkaStreams30 extends KafkaStreams { - - private TestToplogyInfoOneArg internalTopologyBuilder; - - TestKafkaStreams30(String... topics) { - this(new TestToplogyInfoOneArg(topics)); - } - - TestKafkaStreams30(TestToplogyInfoOneArg internalTopologyBuilder) { - super(STREAM_BUILDER.build(), STREAM_PROPS); - this.internalTopologyBuilder = internalTopologyBuilder; - } - } - - /** - * A test version of KafkaStreams as it exists in kafka-streams [3.1,3.2] w/ a topology - * info field named 'topologyMetadata' that in turn has a single-arg version - * of sourceTopicsForStore(String)' that holds the info we need. - */ - static class TestKafkaStreams31_32 extends KafkaStreams { - - private TestToplogyInfoOneArg topologyMetadata; - - TestKafkaStreams31_32(String... topics) { - super(STREAM_BUILDER.build(), STREAM_PROPS); - this.topologyMetadata = new TestToplogyInfoOneArg(topics); - } - } - - /** - * A test version of KafkaStreams as it exists in kafka-streams 3.3+ w/ a topology - * info field named 'topologyMetadata' that in turn has a two-arg version - * of sourceTopicsForStore(String,String)' that holds the info we need. - */ - static class TestKafkaStreams33 extends KafkaStreams { - - private TestToplogyInfoTwoArgs topologyMetadata; - - TestKafkaStreams33(String... topics) { - super(STREAM_BUILDER.build(), STREAM_PROPS); - this.topologyMetadata = new TestToplogyInfoTwoArgs(topics); - } - } - - static class TestToplogyInfoOneArg { - private String[] sourceTopics; - - TestToplogyInfoOneArg(String... sourceTopics) { - this.sourceTopics = sourceTopics; - } - - public Collection sourceTopicsForStore(String storeName) { - return this.sourceTopics == null ? Collections.emptyList() : Arrays.asList(this.sourceTopics); - } - } - - static class TestToplogyInfoTwoArgs { - private String[] sourceTopics; - - TestToplogyInfoTwoArgs(String... sourceTopics) { - this.sourceTopics = sourceTopics; - } - - public Collection sourceTopicsForStore(String storeName, String topologyName) { - return this.sourceTopics == null ? Collections.emptyList() : Arrays.asList(this.sourceTopics); - } - } - - /** - * A test version of KafkaStreams w/ a topology info field named 'internalTopologyBuilder' - * that in turn has NO 'sourceTopicsForStore' method to use. - */ - static class TestKafkaStreamsNoSourceTopicsMethod extends KafkaStreams { - - private TestTopologyInfoNoSourceTopicsMethod internalTopologyBuilder = new TestTopologyInfoNoSourceTopicsMethod(); - - TestKafkaStreamsNoSourceTopicsMethod() { - super(STREAM_BUILDER.build(), STREAM_PROPS); - } - - static class TestTopologyInfoNoSourceTopicsMethod { - - } - - } - - /** - * A test version of KafkaStreams w/ a topology info field named 'internalTopologyBuilder' - * that in turn has a single-arg version of sourceTopicsForStore(String)' that throws - * an exception when invoked. - */ - static class TestKafkaStreamsThrowsError extends KafkaStreams { - - private TestToplogyInfoOneArg internalTopologyBuilder = new TestToplogyInfoOneArg() { - @Override - public Collection sourceTopicsForStore(String storeName) { - throw new RuntimeException("BOOM: " + storeName); - } - }; - - TestKafkaStreamsThrowsError() { - super(STREAM_BUILDER.build(), STREAM_PROPS); - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/MultipleFunctionsInSameAppTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/MultipleFunctionsInSameAppTests.java deleted file mode 100644 index 22a0efabd..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/MultipleFunctionsInSameAppTests.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.core.ResolvableType; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka(topics = {"coffee", "electronics"}) -public class MultipleFunctionsInSameAppTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - private static CountDownLatch countDownLatch = new CountDownLatch(2); - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("purchase-groups", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "coffee", "electronics"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - @SuppressWarnings("unchecked") - void testMultiFunctionsInSameApp() throws InterruptedException { - SpringApplication app = new SpringApplication(MultipleFunctionsInSameApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=processItem;analyze;anotherProcess;yetAnotherProcess", - "--spring.cloud.stream.bindings.processItem-in-0.destination=purchases", - "--spring.cloud.stream.bindings.processItem-out-0.destination=coffee", - "--spring.cloud.stream.bindings.processItem-out-1.destination=electronics", - "--spring.cloud.stream.bindings.analyze-in-0.destination=coffee", - "--spring.cloud.stream.bindings.analyze-in-1.destination=electronics", - "--spring.cloud.stream.kafka.streams.binder.functions.analyze.applicationId=analyze-id-0", - "--spring.cloud.stream.kafka.streams.binder.functions.processItem.applicationId=processItem-id-0", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.bindings.processItem-in-0.consumer.concurrency=2", - "--spring.cloud.stream.bindings.analyze-in-0.consumer.concurrency=1", - "--spring.cloud.stream.kafka.streams.binder.configuration.num.stream.threads=3", - "--spring.cloud.stream.kafka.streams.binder.functions.processItem.configuration.client.id=processItem-client", - "--spring.cloud.stream.kafka.streams.binder.functions.analyze.configuration.client.id=analyze-client", - "--spring.cloud.stream.kafka.streams.binder.functions.anotherProcess.configuration.client.id=anotherProcess-client", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - receiveAndValidate("purchases", "coffee", "electronics"); - - StreamsBuilderFactoryBean processStreamsBuilderFactoryBean = context - .getBean("&stream-builder-processItem", StreamsBuilderFactoryBean.class); - - StreamsBuilderFactoryBean analyzeStreamsBuilderFactoryBean = context - .getBean("&stream-builder-analyze", StreamsBuilderFactoryBean.class); - - StreamsBuilderFactoryBean anotherProcessStreamsBuilderFactoryBean = context - .getBean("&stream-builder-anotherProcess", StreamsBuilderFactoryBean.class); - - final Properties processStreamsConfiguration = processStreamsBuilderFactoryBean.getStreamsConfiguration(); - final Properties analyzeStreamsConfiguration = analyzeStreamsBuilderFactoryBean.getStreamsConfiguration(); - final Properties anotherProcessStreamsConfiguration = anotherProcessStreamsBuilderFactoryBean.getStreamsConfiguration(); - - assertThat(processStreamsConfiguration.getProperty("client.id")).isEqualTo("processItem-client"); - assertThat(analyzeStreamsConfiguration.getProperty("client.id")).isEqualTo("analyze-client"); - - Integer concurrency = (Integer) processStreamsConfiguration.get(StreamsConfig.NUM_STREAM_THREADS_CONFIG); - assertThat(concurrency).isEqualTo(2); - concurrency = (Integer) analyzeStreamsConfiguration.get(StreamsConfig.NUM_STREAM_THREADS_CONFIG); - assertThat(concurrency).isEqualTo(1); - assertThat(anotherProcessStreamsConfiguration.get(StreamsConfig.NUM_STREAM_THREADS_CONFIG)).isEqualTo("3"); - - final KafkaStreamsBindingInformationCatalogue catalogue = context.getBean(KafkaStreamsBindingInformationCatalogue.class); - Field field = ReflectionUtils.findField(KafkaStreamsBindingInformationCatalogue.class, "outboundKStreamResolvables", Map.class); - ReflectionUtils.makeAccessible(field); - final Map outboundKStreamResolvables = (Map) ReflectionUtils.getField(field, catalogue); - // Since we have 2 functions with return types -- one is an array return type with 2 bindings -- assert that - // the catalogue contains outbound type information for all the 3 different bindings. - assertThat(outboundKStreamResolvables.size()).isEqualTo(3); - } - } - - @Test - void testMultiFunctionsInSameAppWithMultiBinders() throws Exception { - SpringApplication app = new SpringApplication(MultipleFunctionsInSameApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=processItem;analyze", - "--spring.cloud.stream.bindings.processItem-in-0.destination=purchases", - "--spring.cloud.stream.kafka.streams.bindings.processItem-in-0.consumer.startOffset=latest", - "--spring.cloud.stream.bindings.processItem-in-0.binder=kafka1", - "--spring.cloud.stream.bindings.processItem-out-0.destination=coffee", - "--spring.cloud.stream.bindings.processItem-out-0.binder=kafka1", - "--spring.cloud.stream.bindings.processItem-out-1.destination=electronics", - "--spring.cloud.stream.bindings.processItem-out-1.binder=kafka1", - "--spring.cloud.stream.bindings.analyze-in-0.destination=coffee", - "--spring.cloud.stream.bindings.analyze-in-0.binder=kafka2", - "--spring.cloud.stream.bindings.analyze-in-1.destination=electronics", - "--spring.cloud.stream.bindings.analyze-in-1.binder=kafka2", - "--spring.cloud.stream.bindings.analyze-in-0.consumer.concurrency=2", - "--spring.cloud.stream.binders.kafka1.type=kstream", - "--spring.cloud.stream.binders.kafka1.environment.spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.binders.kafka1.environment.spring.cloud.stream.kafka.streams.binder.applicationId=my-app-1", - "--spring.cloud.stream.binders.kafka1.environment.spring.cloud.stream.kafka.streams.binder.configuration.client.id=processItem-client", - "--spring.cloud.stream.binders.kafka2.type=kstream", - "--spring.cloud.stream.binders.kafka2.environment.spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.binders.kafka2.environment.spring.cloud.stream.kafka.streams.binder.applicationId=my-app-2", - "--spring.cloud.stream.binders.kafka2.environment.spring.cloud.stream.kafka.streams.binder.configuration.client.id=analyze-client")) { - - Thread.sleep(1000); - receiveAndValidate("purchases", "coffee", "electronics"); - - StreamsBuilderFactoryBean processStreamsBuilderFactoryBean = context - .getBean("&stream-builder-processItem", StreamsBuilderFactoryBean.class); - - StreamsBuilderFactoryBean analyzeStreamsBuilderFactoryBean = context - .getBean("&stream-builder-analyze", StreamsBuilderFactoryBean.class); - - final Properties processStreamsConfiguration = processStreamsBuilderFactoryBean.getStreamsConfiguration(); - final Properties analyzeStreamsConfiguration = analyzeStreamsBuilderFactoryBean.getStreamsConfiguration(); - - assertThat(processStreamsConfiguration.getProperty("application.id")).isEqualTo("my-app-1"); - assertThat(analyzeStreamsConfiguration.getProperty("application.id")).isEqualTo("my-app-2"); - assertThat(processStreamsConfiguration.getProperty("client.id")).isEqualTo("processItem-client"); - assertThat(analyzeStreamsConfiguration.getProperty("client.id")).isEqualTo("analyze-client"); - - Integer concurrency = (Integer) analyzeStreamsConfiguration.get(StreamsConfig.NUM_STREAM_THREADS_CONFIG); - assertThat(concurrency).isEqualTo(2); - - concurrency = (Integer) processStreamsConfiguration.get(StreamsConfig.NUM_STREAM_THREADS_CONFIG); - assertThat(concurrency).isNull(); //thus default to 1 by Kafka Streams. - } - } - - private void receiveAndValidate(String in, String... out) throws InterruptedException { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic(in); - template.sendDefault("coffee"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, out[0]); - assertThat(cr.value().contains("coffee")).isTrue(); - - template.sendDefault("electronics"); - cr = KafkaTestUtils.getSingleRecord(consumer, out[1]); - assertThat(cr.value().contains("electronics")).isTrue(); - - Assert.isTrue(countDownLatch.await(5, TimeUnit.SECONDS), "Analyze (BiConsumer) method didn't receive all the expected records"); - } - finally { - pf.destroy(); - } - } - - @EnableAutoConfiguration - public static class MultipleFunctionsInSameApp { - - @Bean - public Function, KStream[]> processItem() { - return input -> input.branch( - (s, p) -> p.equalsIgnoreCase("coffee"), - (s, p) -> p.equalsIgnoreCase("electronics")); - } - - @Bean - public Function, KStream> yetAnotherProcess() { - return input -> input.map((k, v) -> new KeyValue<>("foo", 1L)); - } - - @Bean - public BiConsumer, KStream> analyze() { - return (coffee, electronics) -> { - coffee.foreach((s, p) -> countDownLatch.countDown()); - electronics.foreach((s, p) -> countDownLatch.countDown()); - }; - } - - @Bean - public java.util.function.Consumer> anotherProcess() { - return c -> { - - }; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/SerdeResolverUtilsTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/SerdeResolverUtilsTests.java deleted file mode 100644 index 49eda7809..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/SerdeResolverUtilsTests.java +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.streams; - -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.UUID; -import java.util.stream.Stream; - -import org.apache.kafka.common.serialization.Deserializer; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.serialization.Serializer; -import org.apache.kafka.common.utils.Bytes; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.kafka.support.serializer.JsonSerde; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.mock; - -/** - * Unit tests for {@link SerdeResolverUtils}. - * - * @author Chris Bono - */ -@SuppressWarnings({ "rawtypes", "NewClassNamingConvention", "unchecked" }) -class SerdeResolverUtilsTests { - - @Nested - class ResolveForType { - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.cloud.function.ineligible-definitions: sendToDlqAndContinue"); - - private Serde fallback = mock(Serde.class); - - @Test - void returnsFallbackSerdeForWildcard() { - this.contextRunner - .withConfiguration(AutoConfigurations.of(SerdeResolverSimpleTestApp.class)) - .run((context) -> { - ResolvableType wildcardType = ResolvableType.forClass(Serde.class).getGeneric(0); - assertThat(SerdeResolverUtils.resolveForType(context, wildcardType, fallback)).isSameAs(fallback); - }); - } - - @Test - void returnsSerdeBeanForMatchingType() { - this.contextRunner - .withConfiguration(AutoConfigurations.of(SerdeResolverSimpleTestApp.class)) - .run((context) -> { - ResolvableType fooType = ResolvableType.forClass(Foo.class); - assertThat(SerdeResolverUtils.resolveForType(context, fooType, fallback)).isInstanceOf(FooSerde.class); - }); - } - - @Nested - class NoMatchingSerdeBeans { - - @ParameterizedTest - @MethodSource("kafkaStreamsBuiltInTypes") - void returnsStandardSerdeForKafkaStreamsBuiltInType(Class builtInType, Serde expectedBuiltInSerde) { - contextRunner.run((context) -> - assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(builtInType), fallback)) - .isInstanceOf(expectedBuiltInSerde.getClass())); - } - - static Stream kafkaStreamsBuiltInTypes() { - return Stream.of( - arguments(String.class, Serdes.String()), - arguments(Short.class, Serdes.Short()), - arguments(Integer.class, Serdes.Integer()), - arguments(Long.class, Serdes.Long()), - arguments(Float.class, Serdes.Float()), - arguments(Double.class, Serdes.Double()), - arguments(byte[].class, Serdes.ByteArray()), - arguments(ByteBuffer.class, Serdes.ByteBuffer()), - arguments(Bytes.class, Serdes.Bytes()), - arguments(UUID.class, Serdes.UUID()) - ); - } - - @Nested - class ForNonKafkaStreamsBuiltInType { - - @Test - void returnsFallbackSerdeWhenValidFallbackSpecified() { - contextRunner.run((context) -> - assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Foo.class), fallback)) - .isSameAs(fallback)); - } - - @ParameterizedTest - @MethodSource("invalidFallbackSerdeProvider") - void returnsJsonSerdeWhenInvalidFallbackSpecified(Serde invalidFallback) { - contextRunner.run((context) -> - assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Foo.class), invalidFallback)) - .isInstanceOf(JsonSerde.class)); - } - - static Stream> invalidFallbackSerdeProvider() { - return Stream.of( - Serdes.String(), - Serdes.Short(), - Serdes.Integer(), - Serdes.Long(), - Serdes.Float(), - Serdes.Double(), - Serdes.ByteArray(), - Serdes.ByteBuffer(), - Serdes.Bytes(), - Serdes.UUID() - ); - } - - @Test - void returnsJsonSerdeWhenFallbackNotSpecified() { - contextRunner.run((context -> - assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Foo.class), null)) - .isInstanceOf(JsonSerde.class))); - } - - @Test - void returnsFallbackSerdeForJavaLangObject() { - // This is an edge case as the only way to get to the JsonSerde step in the 1st place is when - // no fallback is specified or a fallback is specified but its invalid (for a built-in type). - // We will use the 'fallback is not specified' scenario for this test. - contextRunner.run((context) -> - assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Object.class), null)) - .isNull()); - } - } - } - } - - @Nested - class BeanNamesForMatchingSerdes { - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.cloud.function.ineligible-definitions: sendToDlqAndContinue"); - - @Test - void returnsNoSerdesForWildcardType() { - this.contextRunner.withUserConfiguration(SerdeResolverSimpleTestApp.class) - .run((context) -> { - ResolvableType wildcardType = ResolvableType.forClass(Serde.class).getGeneric(0); - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, wildcardType)).isEmpty(); - }); - } - - /** - * - * Verify that {@link SerdeResolverUtils#beanNamesForMatchingSerdes} returns the proper serdes in the proper order - * for the following grid: - *


- * NOTE: {@code GE = GenericEvent} - *

{@code
-		 * ------------------------------------------------------------------
-		 * KStream type       | Serde type
-		 * ------------------------------------------------------------------
-		 *                    | GE | GE | GE | GE
-		 * ------------------------------------------------------------------
-		 * GE           | 1        | -                  | -     | -
-		 * GE | 2        | 1                  | -     | -
-		 * GE              | 4        | 2                  | 1     | 3
-		 * GE                 | 1        | -                  | -     | 2
-		 * ------------------------------------------------------------------
-		 * }
- *


- * NOTE: On the last row, one might expect the {@code GE} serde to be the top match with the {@code GE} - * kstream. However, that is not the case because {@code GE} is a parameterized type and therefore when - * specified as a raw type (without any type info) it resolves to {@code GE} which throws off the ordering. - * A best practice is to specify the type info (even if it is wildcard) for KStream parameterized types. - */ - @Test - void returnsProperlyOrderedSerdesForSimpleGenericTypes() { - - ResolvableType geDate = ResolvableType.forType(new ParameterizedTypeReference>() { }); - ResolvableType geBounded = ResolvableType.forType(new ParameterizedTypeReference>() { }); - ResolvableType geWildcard = ResolvableType.forType(new ParameterizedTypeReference>() { }); - ResolvableType geRaw = ResolvableType.forRawClass(GenericEvent.class); - - this.contextRunner.withUserConfiguration(SerdeResolverSimpleTestApp.class) - .run((context) -> { - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geDate)) - .containsExactly( - "geDateSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geBounded)) - .containsExactly( - "geDateBoundedSerde", - "geDateSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geWildcard)) - .containsExactly( - "geWildcardSerde", - "geDateBoundedSerde", - "geRawSerde", - "geDateSerde", - "geStringSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geRaw)) - .containsExactly( - "geDateSerde", - "geStringSerde", - "geRawSerde"); - }); - } - - /** - * Verify that {@link SerdeResolverUtils#beanNamesForMatchingSerdes} returns the proper serdes in the proper order - * for the following grid: - *


- * NOTE: {@code GE = GenericEvent} - *

{@code
-		 * -------------------------------------------------------------------------------------------------------------------
-		 * KStream type | Serde type
-		 * -------------------------------------------------------------------------------------------------------------------
-		 *              | GE> | GE> | GE> | GE | GE> | GE> | GE> | GE | GE | GE
-		 * -------------------------------------------------------------------------------------------------------------------
-		 * GE>     | 1        | -         | -        | -     | -         | -          | -         | -      | -     | -
-		 * GE>    | 2        | 1         | -        | -     | -         | -          | -         | -      | -     | -
-		 * GE>     | 4        | 3         | 1        | 2     | -         | -          | -         | -      | -     | -
-		 * GE        | 2        | -         | -        | 1     | -         | -          | -         | -      | -     | -
-		 * GE>    | 2        | -         | -        | -     | 1         | -          | -         | -      | -     | -
-		 * GE>   | 3        | 4         | -        | -     | 2         | 1          | -         | -      | -     | -
-		 * GE>    | 7        | 8         | 5        | 6     | 4         | 3          | 1         | 2      | -     | -
-		 * GE       | 4        | -         | -        | 3     | 2         | -          | -         | 1      | -     | -
-		 * GE        | 7        | 8         | 9        | 10    | 2         | 3          | 4         | 5      | 1     | 6
-		 * GE           | 1        | 2         | 3        | 4     | -         | -          | -         | -      | -     | 5
-		 * -------------------------------------------------------------------------------------------------------------------
-		 * }
- *


- * NOTE: On the last row, one might expect the {@code GE} serde to be the top match with the {@code GE} - * kstream. However, that is not the case because {@code GE} is a parameterized type and therefore when - * specified as a raw type (without any type info) it resolves to {@code GE} which throws off the ordering. - * A best practice is to specify the type info (even if it is wildcard) for KStream parameterized types. - */ - @Test - void returnsProperlyOrderedSerdesForComplexGenericTypes() { - - ResolvableType geFooDate = ResolvableType.forType(new ParameterizedTypeReference>>() { }); - ResolvableType geFooDateBounded = ResolvableType.forType(new ParameterizedTypeReference>>() { }); - ResolvableType geFooWildcard = ResolvableType.forType(new ParameterizedTypeReference>>() { }); - ResolvableType geFooRaw = ResolvableType.forType(new ParameterizedTypeReference>() { }); - ResolvableType geFooBoundedDate = ResolvableType.forType(new ParameterizedTypeReference>>() { }); - ResolvableType geFooBoundedDateBounded = ResolvableType.forType(new ParameterizedTypeReference>>() { }); - ResolvableType geFooBoundedWildcard = ResolvableType.forType(new ParameterizedTypeReference>>() { }); - ResolvableType geFooBoundedRaw = ResolvableType.forType(new ParameterizedTypeReference>() { }); - ResolvableType geWildcard = ResolvableType.forType(new ParameterizedTypeReference>() { }); - ResolvableType geRaw = ResolvableType.forRawClass(GenericEvent.class); - - this.contextRunner.withUserConfiguration(SerdeResolverComplexTestApp.class) - .run((context) -> { - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooDate)) - .containsExactly( - "geFooDateSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooDateBounded)) - .containsExactly( - "geFooDateBoundedSerde", - "geFooDateSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooWildcard)) - .containsExactly( - "geFooWildcardSerde", - "geFooRawSerde", - "geFooDateBoundedSerde", - "geFooDateSerde", - "geFooStringSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooRaw)) - .containsExactly( - "geFooRawSerde", - "geFooDateSerde", - "geFooStringSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedDate)) - .containsExactly( - "geFooBoundedDateSerde", - "geFooDateSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedDateBounded)) - .containsExactly( - "geFooBoundedDateBoundedSerde", - "geFooBoundedDateSerde", - "geFooDateSerde", - "geFooDateBoundedSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedWildcard)) - .containsExactly( - "geFooBoundedWildcardSerde", - "geFooBoundedRawSerde", - "geFooBoundedDateBoundedSerde", - "geFooBoundedDateSerde", - "geFooBoundedStringSerde", - "geFooWildcardSerde", - "geFooRawSerde", - "geFooDateSerde", - "geFooDateBoundedSerde", - "geFooStringSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedRaw)) - .containsExactly( - "geFooBoundedRawSerde", - "geFooBoundedDateSerde", - "geFooBoundedStringSerde", - "geFooRawSerde", - "geFooDateSerde", - "geFooStringSerde"); - - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geWildcard)) - .containsExactly( - "geWildcardSerde", - "geFooBoundedDateSerde", - "geFooBoundedDateBoundedSerde", - "geFooBoundedStringSerde", - "geFooBoundedWildcardSerde", - "geFooBoundedRawSerde", - "geRawSerde", - "geFooDateSerde", - "geFooDateBoundedSerde", - "geFooStringSerde", - "geFooWildcardSerde", - "geFooRawSerde"); - - // One might expect geRawSerde to win in order, but it does not because GE is a parameterized type and - // therefore GE resolves to GE which throws off the ordering. Bottom line, for parameterized types - // be sure to specify a type (even if it's wildcard) in the KStream definition. - assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geRaw)) - .containsExactly( - "geFooDateSerde", - "geFooDateBoundedSerde", - "geFooStringSerde", - "geFooWildcardSerde", - "geFooRawSerde", - "geRawSerde"); - }); - } - } - - static class GenericEventSerde implements Serde> { - private String name; - - GenericEventSerde(String name) { - this.name = name; - } - - @Override - public Serializer> serializer() { - return null; - } - - @Override - public Deserializer> deserializer() { - return null; - } - - @Override - public String toString() { - return "GenericEventSerde(" + name + ")"; - } - } - - static class GenericEvent { } - - static class FooSerde implements Serde { - - @Override - public Serializer serializer() { - return null; - } - - @Override - public Deserializer deserializer() { - return null; - } - } - - static class Foo { } - - @EnableAutoConfiguration - static class SerdeResolverSimpleTestApp { - - @Bean - public Serde> geDateSerde() { - return new GenericEventSerde("geDateSerde"); - } - - @Bean - public Serde> geDateBoundedSerde() { - return new GenericEventSerde("geDateBoundedSerde"); - } - - @Bean - public Serde> geStringSerde() { - return new GenericEventSerde("geStringSerde"); - } - - @Bean - public Serde> geWildcardSerde() { - return new GenericEventSerde("geWildcardSerde"); - } - - @Bean - public Serde geRawSerde() { - return new GenericEventSerde("geRawSerde"); - } - - @Bean - public Serde widlcardSerde() { - return Serdes.Void(); - } - - @Bean - public Serde fooSerde() { - return new FooSerde(); - } - } - - @EnableAutoConfiguration - static class SerdeResolverComplexTestApp { - - @Bean - public Serde>> geFooDateSerde() { - return new GenericEventSerde("geFooDateSerde"); - } - - @Bean - public Serde>> geFooDateBoundedSerde() { - return new GenericEventSerde("geFooDateBoundedSerde"); - } - - @Bean - public Serde>> geFooStringSerde() { - return new GenericEventSerde("geFooStringSerde"); - } - - @Bean - public Serde>> geFooWildcardSerde() { - return new GenericEventSerde("geFooWildcardSerde"); - } - - @Bean - public Serde> geFooRawSerde() { - return new GenericEventSerde("geFooRawSerde"); - } - - @Bean - public Serde>> geFooBoundedDateSerde() { - return new GenericEventSerde("geFooBoundedDateSerde"); - } - - @Bean - public Serde>> geFooBoundedDateBoundedSerde() { - return new GenericEventSerde("geFooBoundedDateBoundedSerde"); - } - - @Bean - public Serde>> geFooBoundedStringSerde() { - return new GenericEventSerde("geFooBoundedStringSerde"); - } - - @Bean - public Serde>> geFooBoundedWildcardSerde() { - return new GenericEventSerde("geFooBoundedWildcardSerde"); - } - - @Bean - public Serde> geFooBoundedRawSerde() { - return new GenericEventSerde("geFooBoundedRawSerde"); - } - - @Bean - public Serde> geWildcardSerde() { - return new GenericEventSerde("geWildcardSerde"); - } - - @Bean - public Serde geRawSerde() { - return new GenericEventSerde("geRawSerde"); - } - - @Bean - public Serde widlcardSerde() { - return Serdes.Void(); - } - - @Bean - public Serde fooSerde() { - return new FooSerde(); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/bootstrap/KafkaStreamsBinderBootstrapTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/bootstrap/KafkaStreamsBinderBootstrapTest.java deleted file mode 100644 index 4746c7c28..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/bootstrap/KafkaStreamsBinderBootstrapTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.bootstrap; - -import java.util.Map; -import java.util.Properties; -import java.util.function.Consumer; - -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.type.TypeFactory; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.security.JaasUtils; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.kafka.streams.KeyValueSerdeResolver; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -/** - * Integration tests to verify the bootstrap of a SpringBoot application using the KafkaStreams binder. - * - * @author Soby Chacko - * @author Eduard DomΓ­nguez - * @author Chris Bono - */ -@EmbeddedKafka -class KafkaStreamsBinderBootstrapTest { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @BeforeEach - public void before() { - System.clearProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - } - - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void kafkaStreamsBinderWithStandardConfigCanStart(boolean excludeKafkaAutoConfig) { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleKafkaStreamsApplication.class) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=input1;input2;input3", - "--spring.cloud.stream.kafka.streams.bindings.input1-in-0.consumer.application-id" - + "=testKafkaStreamsBinderWithStandardConfigurationCanStart", - "--spring.cloud.stream.kafka.streams.bindings.input2-in-0.consumer.application-id" - + "=testKafkaStreamsBinderWithStandardConfigurationCanStart-foo", - "--spring.cloud.stream.kafka.streams.bindings.input3-in-0.consumer.application-id" - + "=testKafkaStreamsBinderWithStandardConfigurationCanStart-foobar", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString(), - excludeKafkaAutoConfigParam(excludeKafkaAutoConfig))) { // @checkstyle:off - } // @checkstyle:on - } - - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void kafkaStreamsBinderWithCustomConfigCanStart(boolean excludeKafkaAutoConfig) { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleKafkaStreamsApplication.class) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=input1;input2;input3", - "--spring.cloud.stream.kafka.streams.bindings.input1-in-0.consumer.application-id" - + "=testKStreamBinderWithCustomEnvironmentCanStart", - "--spring.cloud.stream.kafka.streams.bindings.input2-in-0.consumer.application-id" - + "=testKStreamBinderWithCustomEnvironmentCanStart-foo", - "--spring.cloud.stream.kafka.streams.bindings.input3-in-0.consumer.application-id" - + "=testKStreamBinderWithCustomEnvironmentCanStart-foobar", - "--spring.cloud.stream.bindings.input1-in-0.destination=foo", - "--spring.cloud.stream.bindings.input1-in-0.binder=kstreamBinder", - "--spring.cloud.stream.binders.kstreamBinder.type=kstream", - "--spring.cloud.stream.binders.kstreamBinder.environment" - + ".spring.cloud.stream.kafka.streams.binder.brokers" - + "=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.bindings.input2-in-0.destination=bar", - "--spring.cloud.stream.bindings.input2-in-0.binder=ktableBinder", - "--spring.cloud.stream.binders.ktableBinder.type=ktable", - "--spring.cloud.stream.binders.ktableBinder.environment" - + ".spring.cloud.stream.kafka.streams.binder.brokers" - + "=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.bindings.input3-in-0.destination=foobar", - "--spring.cloud.stream.bindings.input3-in-0.binder=globalktableBinder", - "--spring.cloud.stream.binders.globalktableBinder.type=globalktable", - "--spring.cloud.stream.binders.globalktableBinder.environment" - + ".spring.cloud.stream.kafka.streams.binder.brokers" - + "=" + embeddedKafka.getBrokersAsString(), - excludeKafkaAutoConfigParam(excludeKafkaAutoConfig))) { // @checkstyle:off - } // @checkstyle:on - } - - private String excludeKafkaAutoConfigParam(boolean excludeKafkaAutoConfig) { - return excludeKafkaAutoConfig ? - "--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration" : "foo=bar"; - } - - @Test - @SuppressWarnings("unchecked") - void testStreamConfigGlobalProperties_GH1149() { - ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder( - SimpleKafkaStreamsApplication.class).web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=input1;input2;input3", - "--spring.cloud.stream.kafka.streams.bindings.input1-in-0.consumer.application-id" - + "=testKafkaStreamsBinderWithStandardConfigurationCanStart", - "--spring.cloud.stream.kafka.streams.bindings.input2-in-0.consumer.application-id" - + "=testKafkaStreamsBinderWithStandardConfigurationCanStart-foo", - "--spring.cloud.stream.kafka.streams.bindings.input2-in-0.consumer.configuration.spring.json.value.type.method=" + this.getClass().getName() + ".determineType", - "--spring.cloud.stream.kafka.streams.bindings.input3-in-0.consumer.application-id" - + "=testKafkaStreamsBinderWithStandardConfigurationCanStart-foobar", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - - Map streamConfigGlobalProperties = applicationContext - .getBean("streamConfigGlobalProperties", Map.class); - // Make sure that global stream configs do not contain individual binding config set on second function. - assertThat(streamConfigGlobalProperties.containsKey("spring.json.value.type.method")).isFalse(); - - // Make sure that only input2 function gets the specific binding property set on it. - final StreamsBuilderFactoryBean input1SBFB = applicationContext.getBean("&stream-builder-input1", StreamsBuilderFactoryBean.class); - final Properties streamsConfiguration1 = input1SBFB.getStreamsConfiguration(); - assertThat(streamsConfiguration1.containsKey("spring.json.value.type.method")).isFalse(); - - final StreamsBuilderFactoryBean input2SBFB = applicationContext.getBean("&stream-builder-input2", StreamsBuilderFactoryBean.class); - final Properties streamsConfiguration2 = input2SBFB.getStreamsConfiguration(); - assertThat(streamsConfiguration2.containsKey("spring.json.value.type.method")).isTrue(); - - final StreamsBuilderFactoryBean input3SBFB = applicationContext.getBean("&stream-builder-input3", StreamsBuilderFactoryBean.class); - final Properties streamsConfiguration3 = input3SBFB.getStreamsConfiguration(); - assertThat(streamsConfiguration3.containsKey("spring.json.value.type.method")).isFalse(); - applicationContext.getBean(KeyValueSerdeResolver.class); - - //TODO: In Kafka Streams 3.1, taskTopology field is removed. Re-evaluate this testing strategy. -// String configuredSerdeTypeResolver = (String) new DirectFieldAccessor(input2SBFB.getKafkaStreams()) -// .getPropertyValue("taskTopology.processorNodes[0].valDeserializer.typeResolver.arg$2"); -// -// assertThat(this.getClass().getName() + ".determineType").isEqualTo(configuredSerdeTypeResolver); -// -// String configuredKeyDeserializerFieldName = ((String) new DirectFieldAccessor(input2SBFB.getKafkaStreams()) -// .getPropertyValue("taskTopology.processorNodes[0].keyDeserializer.typeMapper.classIdFieldName")); -// assertThat(DefaultJackson2JavaTypeMapper.KEY_DEFAULT_CLASSID_FIELD_NAME).isEqualTo(configuredKeyDeserializerFieldName); -// -// String configuredValueDeserializerFieldName = ((String) new DirectFieldAccessor(input2SBFB.getKafkaStreams()) -// .getPropertyValue("taskTopology.processorNodes[0].valDeserializer.typeMapper.classIdFieldName")); -// assertThat(DefaultJackson2JavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME).isEqualTo(configuredValueDeserializerFieldName); - - applicationContext.close(); - } - - public static JavaType determineType(byte[] data, Headers headers) { - return TypeFactory.defaultInstance().constructParametricType(Map.class, String.class, String.class); - } - - @SpringBootApplication - static class SimpleKafkaStreamsApplication extends BaseConfig { - - @Bean - public Consumer> input1() { - return s -> { - // No-op consumer - }; - } - - @Bean - public Consumer, Map>> input2() { - return s -> { - // No-op consumer - }; - } - - } - - // Testing the scenario reported by https://github.com/spring-cloud/spring-cloud-stream/issues/2737 - static class BaseConfig { - - @Bean - public Consumer> input3() { - return s -> { - // No-op consumer - }; - } - } - - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/bootstrap/KafkaStreamsBinderJaasInitTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/bootstrap/KafkaStreamsBinderJaasInitTests.java deleted file mode 100644 index 9a9cdd85b..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/bootstrap/KafkaStreamsBinderJaasInitTests.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2021-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.cloud.stream.binder.kafka.streams.bootstrap; - -import java.util.function.Consumer; - -import javax.security.auth.login.AppConfigurationEntry; - -import org.apache.kafka.common.security.JaasUtils; -import org.apache.kafka.streams.kstream.KStream; -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.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka -public class KafkaStreamsBinderJaasInitTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static String JAVA_LOGIN_CONFIG_PARAM_VALUE; - - @BeforeAll - public static void beforeAll() { - JAVA_LOGIN_CONFIG_PARAM_VALUE = System.getProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - System.clearProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - } - - @AfterAll - public static void afterAll() { - if (JAVA_LOGIN_CONFIG_PARAM_VALUE != null) { - System.setProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM, JAVA_LOGIN_CONFIG_PARAM_VALUE); - } - } - - @BeforeEach - public void before() { - System.clearProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - javax.security.auth.login.Configuration.setConfiguration(null); - } - - @Test - void testKafkaStreamsBinderJaasInitialization() { - ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder( - KafkaStreamsBinderJaasInitTestsApplication.class).web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=foo", - "--spring.cloud.stream.kafka.streams.bindings.foo-in-0.consumer.application-id" - + "=testKafkaStreamsBinderJaasInitialization-jaas-id", - "--spring.cloud.stream.kafka.streams.binder.jaas.loginModule=org.apache.kafka.common.security.plain.PlainLoginModule", - "--spring.cloud.stream.kafka.streams.binder.jaas.options.username=foo", - "--spring.cloud.stream.kafka.streams.binder.jaas.options.password=bar", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - javax.security.auth.login.Configuration configuration = javax.security.auth.login.Configuration - .getConfiguration(); - final AppConfigurationEntry[] kafkaConfiguration = configuration - .getAppConfigurationEntry("KafkaClient"); - assertThat(kafkaConfiguration).hasSize(1); - assertThat(kafkaConfiguration[0].getOptions().get("username")).isEqualTo("foo"); - assertThat(kafkaConfiguration[0].getOptions().get("password")).isEqualTo("bar"); - assertThat(kafkaConfiguration[0].getControlFlag()) - .isEqualTo(AppConfigurationEntry.LoginModuleControlFlag.REQUIRED); - applicationContext.close(); - } - - @SpringBootApplication - static class KafkaStreamsBinderJaasInitTestsApplication { - - @Bean - public Consumer> foo() { - return s -> { - // No-op consumer - }; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBinderWordCountBranchesFunctionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBinderWordCountBranchesFunctionTests.java deleted file mode 100644 index 68ade8680..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBinderWordCountBranchesFunctionTests.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.function; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Date; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.Predicate; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(topics = {"counts", "foo", "bar"}) -public class KafkaStreamsBinderWordCountBranchesFunctionTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() throws Exception { - Map consumerProps = KafkaTestUtils.consumerProps("groupx", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "counts", "foo", "bar"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testKstreamWordCountWithStringInputAndPojoOuput() throws Exception { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.bindings.input.destination=words", - "--spring.cloud.stream.function.bindings.process-out-0=output1", - "--spring.cloud.stream.bindings.output1.destination=counts", - "--spring.cloud.stream.function.bindings.process-out-1=output2", - "--spring.cloud.stream.bindings.output2.destination=foo", - "--spring.cloud.stream.function.bindings.process-out-2=output3", - "--spring.cloud.stream.bindings.output3.destination=bar", - - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.applicationId" + - "=KafkaStreamsBinderWordCountBranchesFunctionTests-abc", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString()); - try { - receiveAndValidate(context); - } - finally { - context.close(); - } - } - - private void receiveAndValidate(ConfigurableApplicationContext context) throws Exception { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words"); - template.sendDefault("english"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "counts"); - assertThat(cr.value().contains("\"word\":\"english\",\"count\":1")).isTrue(); - - template.sendDefault("french"); - template.sendDefault("french"); - cr = KafkaTestUtils.getSingleRecord(consumer, "foo"); - assertThat(cr.value().contains("\"word\":\"french\",\"count\":2")).isTrue(); - - template.sendDefault("spanish"); - template.sendDefault("spanish"); - template.sendDefault("spanish"); - cr = KafkaTestUtils.getSingleRecord(consumer, "bar"); - assertThat(cr.value().contains("\"word\":\"spanish\",\"count\":3")).isTrue(); - } - - static class WordCount { - - private String word; - - private long count; - - private Date start; - - private Date end; - - WordCount(String word, long count, Date start, Date end) { - this.word = word; - this.count = count; - this.start = start; - this.end = end; - } - - public String getWord() { - return word; - } - - public void setWord(String word) { - this.word = word; - } - - public long getCount() { - return count; - } - - public void setCount(long count) { - this.count = count; - } - - public Date getStart() { - return start; - } - - public void setStart(Date start) { - this.start = start; - } - - public Date getEnd() { - return end; - } - - public void setEnd(Date end) { - this.end = end; - } - } - - @EnableAutoConfiguration - public static class WordCountProcessorApplication { - - @Bean - @SuppressWarnings({"unchecked"}) - public Function, KStream[]> process() { - - Predicate isEnglish = (k, v) -> v.word.equals("english"); - Predicate isFrench = (k, v) -> v.word.equals("french"); - Predicate isSpanish = (k, v) -> v.word.equals("spanish"); - - return input -> { - final Map> stringKStreamMap = input - .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .groupBy((key, value) -> value) - .windowedBy(TimeWindows.of(Duration.ofSeconds(5))) - .count(Materialized.as("WordCounts-branch")) - .toStream() - .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, - new Date(key.window().start()), new Date(key.window().end())))) - .split() - .branch(isEnglish) - .branch(isFrench) - .branch(isSpanish) - .noDefaultBranch(); - - return stringKStreamMap.values().toArray(new KStream[0]); - }; - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBinderWordCountFunctionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBinderWordCountFunctionTests.java deleted file mode 100644 index 53f12d9bb..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsBinderWordCountFunctionTests.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.function; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import io.micrometer.core.instrument.MeterRegistry; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.apache.kafka.streams.processor.StreamPartitioner; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.DefaultBinding; -import org.springframework.cloud.stream.binder.kafka.streams.InteractiveQueryService; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsRegistry; -import org.springframework.cloud.stream.binder.kafka.streams.StreamsBuilderFactoryManager; -import org.springframework.cloud.stream.binder.kafka.streams.endpoint.KafkaStreamsTopologyEndpoint; -import org.springframework.cloud.stream.binding.InputBindingLifecycle; -import org.springframework.cloud.stream.binding.OutputBindingLifecycle; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.Lifecycle; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.config.StreamsBuilderFactoryBeanConfigurer; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.util.Assert; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka(topics = {"counts", "counts-1", "counts-2", "counts-5", "counts-6"}) -public class KafkaStreamsBinderWordCountFunctionTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - private final static CountDownLatch LATCH = new CountDownLatch(1); - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "counts", "counts-1", "counts-2", "counts-5", "counts-6"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - @SuppressWarnings("unchecked") - void testBasicKStreamTopologyExecution() throws Exception { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=words", - "--spring.cloud.stream.bindings.process-out-0.destination=counts", - "--spring.cloud.stream.kafka.streams.binder.application-id=testKstreamWordCountFunction", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.consumerProperties.request.timeout.ms=29000", //for testing ...binder.consumerProperties - "--spring.cloud.stream.kafka.streams.binder.consumerProperties.consumer.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer", - "--spring.cloud.stream.kafka.streams.binder.producerProperties.max.block.ms=90000", //for testing ...binder.producerProperties - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.consumedAs=custom-consumer", - "--spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.producedAs=custom-producer", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - receiveAndValidate("words", "counts"); - - final MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - Thread.sleep(100); - - assertThat(meterRegistry.getMeters().stream().anyMatch(m -> m.getId().getName().equals("kafka.stream.thread.poll.records.max"))).isTrue(); - assertThat(meterRegistry.getMeters().stream().anyMatch(m -> m.getId().getName().equals("kafka.consumer.network.io.total"))).isTrue(); - assertThat(meterRegistry.getMeters().stream().anyMatch(m -> m.getId().getName().equals("kafka.producer.record.send.total"))).isTrue(); - assertThat(meterRegistry.getMeters().stream().anyMatch(m -> m.getId().getName().equals("kafka.admin.client.network.io.total"))).isTrue(); - - Assert.isTrue(LATCH.await(5, TimeUnit.SECONDS), "Failed to call customizers"); - //Testing topology endpoint - final KafkaStreamsRegistry kafkaStreamsRegistry = context.getBean(KafkaStreamsRegistry.class); - final KafkaStreamsTopologyEndpoint kafkaStreamsTopologyEndpoint = new KafkaStreamsTopologyEndpoint(kafkaStreamsRegistry); - final List topologies = kafkaStreamsTopologyEndpoint.kafkaStreamsTopologies(); - final String topology1 = topologies.get(0); - final String topology2 = kafkaStreamsTopologyEndpoint.kafkaStreamsTopology("testKstreamWordCountFunction"); - assertThat(topology1).isNotEmpty(); - assertThat(topology1).isEqualTo(topology2); - assertThat(topology1.contains("Source: custom-consumer")).isTrue(); - assertThat(topology1.contains("Sink: custom-producer")).isTrue(); - - //verify that ...binder.consumerProperties and ...binder.producerProperties work. - Map streamConfigGlobalProperties = (Map) context.getBean("streamConfigGlobalProperties"); - assertThat(streamConfigGlobalProperties.get("consumer.request.timeout.ms")).isEqualTo("29000"); - assertThat(streamConfigGlobalProperties.get("consumer.value.deserializer")).isEqualTo("org.apache.kafka.common.serialization.StringDeserializer"); - assertThat(streamConfigGlobalProperties.get("producer.max.block.ms")).isEqualTo("90000"); - - InputBindingLifecycle inputBindingLifecycle = context.getBean(InputBindingLifecycle.class); - final Collection> inputBindings = (Collection>) new DirectFieldAccessor(inputBindingLifecycle) - .getPropertyValue("inputBindings"); - assertThat(inputBindings).isNotNull(); - final Optional> theOnlyInputBinding = inputBindings.stream().findFirst(); - assertThat(theOnlyInputBinding.isPresent()).isTrue(); - final DefaultBinding objectBinding = (DefaultBinding) theOnlyInputBinding.get(); - assertThat(objectBinding.getBindingName()).isEqualTo("process-in-0"); - - final Lifecycle lifecycle = (Lifecycle) new DirectFieldAccessor(objectBinding).getPropertyValue("lifecycle"); - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = context.getBean(StreamsBuilderFactoryBean.class); - assertThat(lifecycle).isEqualTo(streamsBuilderFactoryBean); - - OutputBindingLifecycle outputBindingLifecycle = context.getBean(OutputBindingLifecycle.class); - final Collection> outputBindings = (Collection>) new DirectFieldAccessor(outputBindingLifecycle) - .getPropertyValue("outputBindings"); - assertThat(outputBindings).isNotNull(); - final Optional> theOnlyOutputBinding = outputBindings.stream().findFirst(); - assertThat(theOnlyOutputBinding.isPresent()).isTrue(); - final DefaultBinding objectBinding1 = (DefaultBinding) theOnlyOutputBinding.get(); - assertThat(objectBinding1.getBindingName()).isEqualTo("process-out-0"); - - final Lifecycle lifecycle1 = (Lifecycle) new DirectFieldAccessor(objectBinding1).getPropertyValue("lifecycle"); - assertThat(lifecycle1).isEqualTo(streamsBuilderFactoryBean); - } - } - - @Test - void testKstreamWordCountWithApplicationIdSpecifiedAtDefaultConsumer() { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=words-5", - "--spring.cloud.stream.bindings.process-out-0.destination=counts-5", - "--spring.cloud.stream.kafka.streams.default.consumer.application-id=testKstreamWordCountWithApplicationIdSpecifiedAtDefaultConsumer", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.binder.brokers=" - + embeddedKafka.getBrokersAsString())) { - receiveAndValidate("words-5", "counts-5"); - } - } - - @Test - void testKstreamWordCountFunctionWithCustomProducerStreamPartitioner() throws Exception { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.kafka.streams.binder.application-id=testKstreamWordCountFunctionWithCustomProducerStreamPartitioner", - "--spring.cloud.stream.bindings.process-in-0.destination=words-2", - "--spring.cloud.stream.bindings.process-out-0.destination=counts-2", - "--spring.cloud.stream.bindings.process-out-0.producer.partitionCount=2", - "--spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.streamPartitionerBeanName" + - "=streamPartitioner", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words-2"); - template.sendDefault("foo"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, "counts-2"); - assertThat(cr.value().contains("\"word\":\"foo\",\"count\":1")).isTrue(); - assertThat(cr.partition() == 0) .isTrue(); - template.sendDefault("bar"); - cr = KafkaTestUtils.getSingleRecord(consumer, "counts-2"); - assertThat(cr.value().contains("\"word\":\"bar\",\"count\":1")).isTrue(); - assertThat(cr.partition() == 1) .isTrue(); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testKstreamBinderAutoStartup() throws Exception { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.kafka.streams.auto-startup=false", - "--spring.cloud.stream.bindings.process-in-0.destination=words-3", - "--spring.cloud.stream.bindings.process-out-0.destination=counts-3", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - final StreamsBuilderFactoryManager streamsBuilderFactoryManager = context.getBean(StreamsBuilderFactoryManager.class); - assertThat(streamsBuilderFactoryManager.isAutoStartup()).isFalse(); - assertThat(streamsBuilderFactoryManager.isRunning()).isFalse(); - } - } - - @Test - void testKstreamIndividualBindingAutoStartup() throws Exception { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=words-4", - "--spring.cloud.stream.bindings.process-in-0.consumer.auto-startup=false", - "--spring.cloud.stream.bindings.process-out-0.destination=counts-4", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - final StreamsBuilderFactoryBean streamsBuilderFactoryBean = context.getBean(StreamsBuilderFactoryBean.class); - assertThat(streamsBuilderFactoryBean.isRunning()).isFalse(); - streamsBuilderFactoryBean.start(); - assertThat(streamsBuilderFactoryBean.isRunning()).isTrue(); - } - } - - // The following test verifies the fixes made for this issue: - // https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/774 - @Test - void testOutboundNullValueIsHandledGracefully() - throws Exception { - SpringApplication app = new SpringApplication(OutboundNullApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=words-6", - "--spring.cloud.stream.bindings.process-out-0.destination=counts-6", - "--spring.cloud.stream.bindings.process-out-0.producer.useNativeEncoding=false", - "--spring.cloud.stream.kafka.streams.default.consumer.application-id=testOutboundNullValueIsHandledGracefully", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.binder.brokers=" - + embeddedKafka.getBrokersAsString())) { - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words-6"); - template.sendDefault("foobar"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - "counts-6"); - assertThat(cr.value() == null).isTrue(); - } - finally { - pf.destroy(); - } - } - } - - private void receiveAndValidate(String in, String out) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic(in); - template.sendDefault("foobar"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, out); - assertThat(cr.value().contains("\"word\":\"foobar\",\"count\":1")).isTrue(); - } - finally { - pf.destroy(); - } - } - - static class WordCount { - - private String word; - - private long count; - - private Date start; - - private Date end; - - WordCount(String word, long count, Date start, Date end) { - this.word = word; - this.count = count; - this.start = start; - this.end = end; - } - - public String getWord() { - return word; - } - - public void setWord(String word) { - this.word = word; - } - - public long getCount() { - return count; - } - - public void setCount(long count) { - this.count = count; - } - - public Date getStart() { - return start; - } - - public void setStart(Date start) { - this.start = start; - } - - public Date getEnd() { - return end; - } - - public void setEnd(Date end) { - this.end = end; - } - } - - @EnableAutoConfiguration - public static class WordCountProcessorApplication { - - @Autowired - InteractiveQueryService interactiveQueryService; - - @Bean - Function, KStream> process() { - - return input -> input - .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(Serdes.String(), Serdes.String())) - .windowedBy(TimeWindows.of(Duration.ofMillis(5000))) - .count(Materialized.as("foo-WordCounts")) - .toStream() - .map((key, value) -> new KeyValue<>(key.key(), new WordCount(key.key(), value, - new Date(key.window().start()), new Date(key.window().end())))); - } - - @Bean - StreamsBuilderFactoryBeanConfigurer customizer() { - return fb -> { - try { - fb.setStateListener((newState, oldState) -> { - - }); - fb.getObject(); //make sure no exception is thrown at this call. - KafkaStreamsBinderWordCountFunctionTests.LATCH.countDown(); - - } - catch (Exception e) { - //Nothing to do - When the exception is thrown above, the latch won't be count down. - } - }; - } - - @Bean - StreamPartitioner streamPartitioner() { - return (t, k, v, n) -> k.equals("foo") ? 0 : 1; - } - } - - @EnableAutoConfiguration - static class OutboundNullApplication { - - @Bean - Function, KStream> process() { - return input -> input - .flatMapValues( - value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(Serdes.String(), Serdes.String())) - .windowedBy(TimeWindows.of(Duration.ofSeconds(5))).count(Materialized.as("foobar-WordCounts")) - .toStream() - .map((key, value) -> new KeyValue<>(null, null)); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsComponentBeansTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsComponentBeansTests.java deleted file mode 100644 index c2762b569..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsComponentBeansTests.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright 2021-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.cloud.stream.binder.kafka.streams.function; - -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(topics = {"testFunctionComponent-out", "testBiFunctionComponent-out", "testCurriedFunctionWithFunctionTerminal-out"}) -public class KafkaStreamsComponentBeansTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer1; - private static Consumer consumer2; - private static Consumer consumer3; - - private final static CountDownLatch LATCH_1 = new CountDownLatch(1); - private final static CountDownLatch LATCH_2 = new CountDownLatch(2); - private final static CountDownLatch LATCH_3 = new CountDownLatch(3); - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer1 = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer1, "testFunctionComponent-out"); - - Map consumerProps1 = KafkaTestUtils.consumerProps("group-x", "false", - embeddedKafka); - consumerProps1.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps1.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); - DefaultKafkaConsumerFactory cf1 = new DefaultKafkaConsumerFactory<>(consumerProps1); - consumer2 = cf1.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer2, "testBiFunctionComponent-out"); - - Map consumerProps2 = KafkaTestUtils.consumerProps("group-y", "false", - embeddedKafka); - consumerProps2.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps2.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); - DefaultKafkaConsumerFactory cf2 = new DefaultKafkaConsumerFactory<>(consumerProps2); - consumer3 = cf2.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer3, "testCurriedFunctionWithFunctionTerminal-out"); - } - - @AfterAll - public static void tearDown() { - consumer1.close(); - consumer2.close(); - consumer3.close(); - } - - @Test - void testFunctionComponent() { - SpringApplication app = new SpringApplication(FunctionAsComponent.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.foo-in-0.destination=testFunctionComponent-in", - "--spring.cloud.stream.bindings.foo-out-0.destination=testFunctionComponent-out", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("testFunctionComponent-in"); - template.sendDefault("foobar"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer1, "testFunctionComponent-out"); - assertThat(cr.value().contains("foobarfoobar")).isTrue(); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testConsumerComponent() throws Exception { - SpringApplication app = new SpringApplication(ConsumerAsComponent.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.bar-in-0.destination=testConsumerComponent-in", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("testConsumerComponent-in"); - template.sendDefault("foobar"); - Assert.isTrue(LATCH_1.await(10, TimeUnit.SECONDS), "bar"); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testBiFunctionComponent() { - SpringApplication app = new SpringApplication(BiFunctionAsComponent.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.bazz-in-0.destination=testBiFunctionComponent-in-0", - "--spring.cloud.stream.bindings.bazz-in-1.destination=testBiFunctionComponent-in-1", - "--spring.cloud.stream.bindings.bazz-out-0.destination=testBiFunctionComponent-out", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("testBiFunctionComponent-in-0"); - template.sendDefault("foobar"); - template.setDefaultTopic("testBiFunctionComponent-in-1"); - template.sendDefault("foobar"); - final ConsumerRecords records = KafkaTestUtils.getRecords(consumer2, Duration.ofSeconds(10), 2); - assertThat(records.count()).isEqualTo(2); - records.forEach(stringStringConsumerRecord -> assertThat(stringStringConsumerRecord.value().contains("foobar")).isTrue()); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testBiConsumerComponent() throws Exception { - SpringApplication app = new SpringApplication(BiConsumerAsComponent.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.buzz-in-0.destination=testBiConsumerComponent-in-0", - "--spring.cloud.stream.bindings.buzz-in-1.destination=testBiConsumerComponent-in-1", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("testBiConsumerComponent-in-0"); - template.sendDefault("foobar"); - template.setDefaultTopic("testBiConsumerComponent-in-1"); - template.sendDefault("foobar"); - Assert.isTrue(LATCH_2.await(10, TimeUnit.SECONDS), "bar"); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testCurriedFunctionWithConsumerTerminal() throws Exception { - SpringApplication app = new SpringApplication(CurriedFunctionWithConsumerTerminal.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.curriedConsumer-in-0.destination=testCurriedFunctionWithConsumerTerminal-in-0", - "--spring.cloud.stream.bindings.curriedConsumer-in-1.destination=testCurriedFunctionWithConsumerTerminal-in-1", - "--spring.cloud.stream.bindings.curriedConsumer-in-2.destination=testCurriedFunctionWithConsumerTerminal-in-2", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("testCurriedFunctionWithConsumerTerminal-in-0"); - template.sendDefault("foobar"); - template.setDefaultTopic("testCurriedFunctionWithConsumerTerminal-in-1"); - template.sendDefault("foobar"); - template.setDefaultTopic("testCurriedFunctionWithConsumerTerminal-in-2"); - template.sendDefault("foobar"); - Assert.isTrue(LATCH_3.await(10, TimeUnit.SECONDS), "bar"); - } - finally { - pf.destroy(); - } - } - } - - @Test - void testCurriedFunctionWithFunctionTerminal() { - SpringApplication app = new SpringApplication(CurriedFunctionWithFunctionTerminal.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.curriedFunction-in-0.destination=testCurriedFunctionWithFunctionTerminal-in-0", - "--spring.cloud.stream.bindings.curriedFunction-in-1.destination=testCurriedFunctionWithFunctionTerminal-in-1", - "--spring.cloud.stream.bindings.curriedFunction-in-2.destination=testCurriedFunctionWithFunctionTerminal-in-2", - "--spring.cloud.stream.bindings.curriedFunction-out-0.destination=testCurriedFunctionWithFunctionTerminal-out", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("testCurriedFunctionWithFunctionTerminal-in-0"); - template.sendDefault("foobar"); - template.setDefaultTopic("testCurriedFunctionWithFunctionTerminal-in-1"); - template.sendDefault("foobar"); - template.setDefaultTopic("testCurriedFunctionWithFunctionTerminal-in-2"); - template.sendDefault("foobar"); - final ConsumerRecords records = KafkaTestUtils.getRecords(consumer3, Duration.ofSeconds(10), 3); - assertThat(records.count()).isEqualTo(3); - records.forEach(stringStringConsumerRecord -> assertThat(stringStringConsumerRecord.value().contains("foobar")).isTrue()); - } - finally { - pf.destroy(); - } - } - } - - @Component("foo") - @EnableAutoConfiguration - public static class FunctionAsComponent implements Function, - KStream> { - - @Override - public KStream apply(KStream stringIntegerKStream) { - return stringIntegerKStream.map((integer, s) -> new KeyValue<>(s, s + s)); - } - } - - @Component("bar") - @EnableAutoConfiguration - public static class ConsumerAsComponent implements java.util.function.Consumer> { - - @Override - public void accept(KStream integerStringKStream) { - integerStringKStream.foreach((integer, s) -> LATCH_1.countDown()); - } - } - - @Component("bazz") - @EnableAutoConfiguration - public static class BiFunctionAsComponent implements BiFunction, KStream, KStream> { - - @Override - public KStream apply(KStream stringStringKStream, KStream stringStringKStream2) { - return stringStringKStream.merge(stringStringKStream2); - } - } - - @Component("buzz") - @EnableAutoConfiguration - public static class BiConsumerAsComponent implements BiConsumer, KStream> { - - @Override - public void accept(KStream stringStringKStream, KStream stringStringKStream2) { - final KStream merged = stringStringKStream.merge(stringStringKStream2); - merged.foreach((s, s2) -> LATCH_2.countDown()); - } - } - - @Component("curriedConsumer") - @EnableAutoConfiguration - public static class CurriedFunctionWithConsumerTerminal implements Function, - Function, - java.util.function.Consumer>>> { - - @Override - public Function, java.util.function.Consumer>> apply(KStream stringStringKStream) { - return stringStringKStream1 -> stringStringKStream2 -> { - final KStream merge1 = stringStringKStream.merge(stringStringKStream1); - final KStream merged2 = merge1.merge(stringStringKStream2); - merged2.foreach((s1, s2) -> LATCH_3.countDown()); - }; - } - } - - @Component("curriedFunction") - @EnableAutoConfiguration - public static class CurriedFunctionWithFunctionTerminal implements Function, - Function, - java.util.function.Function, KStream>>> { - - @Override - public Function, Function, KStream>> apply(KStream stringStringKStream) { - return stringStringKStream1 -> stringStringKStream2 -> { - final KStream merge1 = stringStringKStream.merge(stringStringKStream1); - return merge1.merge(stringStringKStream2); - }; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionStateStoreTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionStateStoreTests.java deleted file mode 100644 index dbee924b6..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsFunctionStateStoreTests.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.function; - -import java.time.Duration; -import java.util.Map; - -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.apache.kafka.streams.processor.Processor; -import org.apache.kafka.streams.processor.ProcessorContext; -import org.apache.kafka.streams.processor.ProcessorSupplier; -import org.apache.kafka.streams.state.KeyValueStore; -import org.apache.kafka.streams.state.StoreBuilder; -import org.apache.kafka.streams.state.Stores; -import org.apache.kafka.streams.state.WindowStore; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka -public class KafkaStreamsFunctionStateStoreTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void testKafkaStreamsFuncionWithMultipleStateStores() throws Exception { - SpringApplication app = new SpringApplication(StateStoreTestApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=biConsumerBean;hello", - "--spring.cloud.stream.bindings.biConsumerBean-in-0.destination=words", - "--spring.cloud.stream.bindings.hello-in-0.destination=words", - "--spring.cloud.stream.kafka.streams.binder.functions.changed.applicationId=testKafkaStreamsFuncionWithMultipleStateStores-123", - "--spring.cloud.stream.kafka.streams.binder.functions.hello.applicationId=testKafkaStreamsFuncionWithMultipleStateStores-456", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - receiveAndValidate(context); - } - } - - private void receiveAndValidate(ConfigurableApplicationContext context) throws Exception { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words"); - template.sendDefault(1, "foobar"); - Thread.sleep(2000L); - StateStoreTestApplication processorApplication = context - .getBean(StateStoreTestApplication.class); - - KeyValueStore state1 = processorApplication.state1; - assertThat(processorApplication.processed1).isTrue(); - assertThat(state1 != null).isTrue(); - assertThat(state1.name()).isEqualTo("my-store"); - WindowStore state2 = processorApplication.state2; - assertThat(state2 != null).isTrue(); - assertThat(state2.name()).isEqualTo("other-store"); - assertThat(state2.persistent()).isTrue(); - - KeyValueStore state3 = processorApplication.state1; - assertThat(processorApplication.processed2).isTrue(); - assertThat(state3 != null).isTrue(); - assertThat(state3.name()).isEqualTo("my-store"); - WindowStore state4 = processorApplication.state2; - assertThat(state4 != null).isTrue(); - assertThat(state4.name()).isEqualTo("other-store"); - assertThat(state4.persistent()).isTrue(); - } - finally { - pf.destroy(); - } - } - - @EnableAutoConfiguration - public static class StateStoreTestApplication { - - KeyValueStore state1; - WindowStore state2; - - KeyValueStore state3; - WindowStore state4; - - boolean processed1; - boolean processed2; - - @Bean(name = "biConsumerBean") - public java.util.function.BiConsumer, KStream> process() { - return (input0, input1) -> - input0.process((ProcessorSupplier) () -> new Processor() { - @Override - @SuppressWarnings("unchecked") - public void init(ProcessorContext context) { - state1 = (KeyValueStore) context.getStateStore("my-store"); - state2 = (WindowStore) context.getStateStore("other-store"); - } - - @Override - public void process(Object key, String value) { - processed1 = true; - } - - @Override - public void close() { - - } - }, "my-store", "other-store"); - } - - @Bean - public java.util.function.Consumer> hello() { - return input -> { - input.toStream().process(() -> new Processor() { - @Override - @SuppressWarnings("unchecked") - public void init(ProcessorContext context) { - state3 = (KeyValueStore) context.getStateStore("my-store"); - state4 = (WindowStore) context.getStateStore("other-store"); - } - - @Override - public void process(Object key, String value) { - processed2 = true; - } - - @Override - public void close() { - - } - }, "my-store", "other-store"); - }; - } - - @Bean - public StoreBuilder myStore() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore("my-store"), Serdes.Long(), - Serdes.Long()); - } - - @Bean - public StoreBuilder otherStore() { - return Stores.windowStoreBuilder( - Stores.persistentWindowStore("other-store", - Duration.ofSeconds(3), Duration.ofSeconds(3), false), Serdes.Long(), - Serdes.Long()); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsRetryTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsRetryTests.java deleted file mode 100644 index 3e3c44f25..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/KafkaStreamsRetryTests.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.streams.function; - -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; - -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.apache.kafka.streams.processor.Processor; -import org.apache.kafka.streams.processor.ProcessorContext; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.annotation.StreamRetryTemplate; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.retry.RetryPolicy; -import org.springframework.retry.backoff.FixedBackOffPolicy; -import org.springframework.retry.policy.SimpleRetryPolicy; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; - -@EmbeddedKafka -public class KafkaStreamsRetryTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private final static CountDownLatch LATCH1 = new CountDownLatch(2); - private final static CountDownLatch LATCH2 = new CountDownLatch(4); - - @Test - void testRetryTemplatePerBindingOnKStream() throws Exception { - SpringApplication app = new SpringApplication(RetryTemplatePerConsumerBindingApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.process-in-0.destination=words", - "--spring.cloud.stream.bindings.process-in-0.consumer.max-attempts=2", - "--spring.cloud.stream.kafka.streams.default.consumer.application-id=testRetryTemplatePerBindingOnKStream", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - sendAndValidate(LATCH1); - } - } - - @Test - void testRetryTemplateOnTableTypes() throws Exception { - SpringApplication app = new SpringApplication(RetryTemplatePerConsumerBindingApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=tableTypes", - "--spring.cloud.stream.kafka.streams.default.consumer.application-id=testRetryTemplateOnTableTypes", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - assertThat(context.getBean("tableTypes-in-0-RetryTemplate", RetryTemplate.class)).isNotNull(); - assertThat(context.getBean("tableTypes-in-1-RetryTemplate", RetryTemplate.class)).isNotNull(); - } - } - - @Test - void testRetryTemplateBeanProvidedByTheApp() throws Exception { - SpringApplication app = new SpringApplication(CustomRetryTemplateApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.process-in-0.destination=words", - "--spring.cloud.stream.bindings.process-in-0.consumer.retry-template-name=fooRetryTemplate", - "--spring.cloud.stream.kafka.streams.default.consumer.application-id=testRetryTemplateBeanProvidedByTheApp", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - sendAndValidate(LATCH2); - assertThatThrownBy(() -> context.getBean("process-in-0-RetryTemplate", RetryTemplate.class)).isInstanceOf(NoSuchBeanDefinitionException.class); - } - } - - private void sendAndValidate(CountDownLatch latch) throws InterruptedException { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words"); - template.sendDefault("foobar"); - Assert.isTrue(latch.await(10, TimeUnit.SECONDS), "Foo"); - } - finally { - pf.destroy(); - } - } - - @EnableAutoConfiguration - public static class RetryTemplatePerConsumerBindingApp { - - @Bean - public java.util.function.Consumer> process(@Lazy @Qualifier("process-in-0-RetryTemplate") RetryTemplate retryTemplate) { - - return input -> input - .process(() -> new Processor() { - @Override - public void init(ProcessorContext processorContext) { - } - - @Override - public void process(Object o, String s) { - retryTemplate.execute(context -> { - LATCH1.countDown(); - throw new RuntimeException(); - }); - } - - @Override - public void close() { - } - }); - } - - @Bean - public BiConsumer, GlobalKTable> tableTypes() { - return (t, g) -> { - }; - } - } - - @EnableAutoConfiguration - public static class CustomRetryTemplateApp { - - @Bean - @StreamRetryTemplate - RetryTemplate fooRetryTemplate() { - RetryTemplate retryTemplate = new RetryTemplate(); - - RetryPolicy retryPolicy = new SimpleRetryPolicy(4); - FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); - backOffPolicy.setBackOffPeriod(1); - - retryTemplate.setBackOffPolicy(backOffPolicy); - retryTemplate.setRetryPolicy(retryPolicy); - - return retryTemplate; - } - - @Bean - public java.util.function.Consumer> process() { - - return input -> input - .process(() -> new Processor() { - @Override - public void init(ProcessorContext processorContext) { - } - - @Override - public void process(Object o, String s) { - fooRetryTemplate().execute(context -> { - LATCH2.countDown(); - throw new RuntimeException(); - }); - - } - - @Override - public void close() { - } - }); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/SerdesProvidedAsBeansTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/SerdesProvidedAsBeansTests.java deleted file mode 100644 index 0345d0568..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/SerdesProvidedAsBeansTests.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.function; - -import java.lang.reflect.Method; -import java.util.Date; -import java.util.function.Function; - -import org.apache.kafka.common.serialization.Deserializer; -import org.apache.kafka.common.serialization.Serde; -import org.apache.kafka.common.serialization.Serializer; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.KeyValueSerdeResolver; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsProducerProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.core.ResolvableType; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests to verify proper resolution of {@link Serde serdes} when they are provided as beans. - * - * @author Soby Chako - * @author Chris Bono - */ -@EmbeddedKafka(topics = { "topic1", "topic2" }) -public class SerdesProvidedAsBeansTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void simpleSerdeBeansAreResolvedProperly() throws Exception { - SpringApplication app = new SpringApplication(SerdesProvidedAsBeansTestApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=simpleProcess", - "--spring.cloud.stream.bindings.simpleProcess-in-0.destination=topic1", - "--spring.cloud.stream.bindings.simpleProcess-out-0.destination=topic2", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Method method = SerdesProvidedAsBeansTestApp.class.getMethod("simpleProcess"); - ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, SerdesProvidedAsBeansTestApp.class); - - KeyValueSerdeResolver keyValueSerdeResolver = context.getBean(KeyValueSerdeResolver.class); - - BindingServiceProperties bindingServiceProperties = context.getBean(BindingServiceProperties.class); - ConsumerProperties consumerProperties = bindingServiceProperties.getBindingProperties("simpleProcess-in-0").getConsumer(); - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties = context.getBean(KafkaStreamsExtendedBindingProperties.class); - KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties = kafkaStreamsExtendedBindingProperties.getExtendedConsumerProperties("input"); - Serde inboundValueSerde = keyValueSerdeResolver.getInboundValueSerde(consumerProperties, kafkaStreamsConsumerProperties, resolvableType.getGeneric(0)); - - assertThat(inboundValueSerde).isInstanceOf(FooSerde.class); - - ProducerProperties producerProperties = bindingServiceProperties.getBindingProperties("simpleProcess-out-0").getProducer(); - KafkaStreamsProducerProperties kafkaStreamsProducerProperties = kafkaStreamsExtendedBindingProperties.getExtendedProducerProperties("output"); - Serde outboundValueSerde = keyValueSerdeResolver.getOutboundValueSerde(producerProperties, kafkaStreamsProducerProperties, resolvableType.getGeneric(1)); - - assertThat(outboundValueSerde).isInstanceOf(FooSerde.class); - } - } - - @Test - void genericSerdeBeansAreResolvedProperly() throws Exception { - SpringApplication app = new SpringApplication(SerdesProvidedAsBeansTestApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=genericProcess", - "--spring.cloud.stream.bindings.genericProcess-in-0.destination=topic1", - "--spring.cloud.stream.bindings.genericProcess-out-0.destination=topic2", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - Method method = SerdesProvidedAsBeansTestApp.class.getMethod("genericProcess"); - ResolvableType resolvableType = ResolvableType.forMethodReturnType(method, SerdesProvidedAsBeansTestApp.class); - - KeyValueSerdeResolver keyValueSerdeResolver = context.getBean(KeyValueSerdeResolver.class); - - BindingServiceProperties bindingServiceProperties = context.getBean(BindingServiceProperties.class); - ConsumerProperties consumerProperties = bindingServiceProperties.getBindingProperties("genericProcess-in-0").getConsumer(); - KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties = context.getBean(KafkaStreamsExtendedBindingProperties.class); - KafkaStreamsConsumerProperties kafkaStreamsConsumerProperties = kafkaStreamsExtendedBindingProperties.getExtendedConsumerProperties("input"); - Serde inboundValueSerde = keyValueSerdeResolver.getInboundValueSerde(consumerProperties, kafkaStreamsConsumerProperties, resolvableType.getGeneric(0)); - - assertThat(inboundValueSerde).isInstanceOf(GenericEventDateSerde.class); - - ProducerProperties producerProperties = bindingServiceProperties.getBindingProperties("genericProcess-out-0").getProducer(); - KafkaStreamsProducerProperties kafkaStreamsProducerProperties = kafkaStreamsExtendedBindingProperties.getExtendedProducerProperties("output"); - Serde outboundValueSerde = keyValueSerdeResolver.getOutboundValueSerde(producerProperties, kafkaStreamsProducerProperties, resolvableType.getGeneric(1)); - - assertThat(outboundValueSerde).isInstanceOf(GenericEventStringSerde.class); - } - } - - static class FooSerde implements Serde { - @Override - public Serializer serializer() { - return null; - } - - @Override - public Deserializer deserializer() { - return null; - } - } - - static class GenericEventDateSerde implements Serde> { - @Override - public Serializer> serializer() { - return null; - } - - @Override - public Deserializer> deserializer() { - return null; - } - } - - static class GenericEventStringSerde implements Serde> { - @Override - public Serializer> serializer() { - return null; - } - - @Override - public Deserializer> deserializer() { - return null; - } - } - - static class GenericEvent { - - static GenericEvent of(X newThing) { - GenericEvent newEvent = new GenericEvent<>(); - newEvent.setThing(newThing); - return newEvent; - } - - private T thing; - - public T getThing() { - return thing; - } - - public void setThing(T thing) { - this.thing = thing; - } - } - - @EnableAutoConfiguration - public static class SerdesProvidedAsBeansTestApp { - - @Bean - public Serde> genericEventDataSerde() { - return new GenericEventDateSerde(); - } - - @Bean - public Serde> genericEventStringSerde() { - return new GenericEventStringSerde(); - } - - @Bean - public Serde fooSerde() { - return new FooSerde<>(); - } - - @Bean - public Function, KStream> simpleProcess() { - return input -> input; - } - - @Bean - public Function>, KStream>> genericProcess() { - return input -> input.map((k, v) -> new KeyValue(k, GenericEvent.of(v.toString()))); - } - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/StreamToGlobalKTableFunctionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/StreamToGlobalKTableFunctionTests.java deleted file mode 100644 index 5b3992c51..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/StreamToGlobalKTableFunctionTests.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.function; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.serialization.LongDeserializer; -import org.apache.kafka.common.serialization.LongSerializer; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.GlobalKTable; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.apache.kafka.streams.processor.TimestampExtractor; -import org.apache.kafka.streams.processor.WallclockTimestampExtractor; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsBindingProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsExtendedBindingProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.serializer.JsonDeserializer; -import org.springframework.kafka.support.serializer.JsonSerializer; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka(topics = "enriched-order") -public class StreamToGlobalKTableFunctionTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @Test - void testStreamToGlobalKTable() throws Exception { - SpringApplication app = new SpringApplication(OrderEnricherApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.function.bindings.process-in-0=order", - "--spring.cloud.stream.function.bindings.process-in-1=customer", - "--spring.cloud.stream.function.bindings.process-in-2=product", - "--spring.cloud.stream.function.bindings.process-out-0=enriched-order", - "--spring.cloud.stream.bindings.order.destination=orders", - "--spring.cloud.stream.bindings.customer.destination=customers", - "--spring.cloud.stream.bindings.product.destination=products", - "--spring.cloud.stream.bindings.enriched-order.destination=enriched-order", - - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=10000", - "--spring.cloud.stream.kafka.streams.bindings.order.consumer.applicationId=" + - "StreamToGlobalKTableJoinFunctionTests-abc", - - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.topic.properties.cleanup.policy=compact", - "--spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.topic.properties.cleanup.policy=compact", - "--spring.cloud.stream.kafka.streams.bindings.process-in-2.consumer.topic.properties.cleanup.policy=compact", - - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - // Testing certain ancillary configuration of GlobalKTable around topics creation. - // See this issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/687 - - BinderFactory binderFactory = context.getBeanFactory() - .getBean(BinderFactory.class); - - Binder kStreamBinder = binderFactory - .getBinder("kstream", KStream.class); - - KafkaStreamsConsumerProperties input = (KafkaStreamsConsumerProperties) ((ExtendedPropertiesBinder) kStreamBinder) - .getExtendedConsumerProperties("process-in-0"); - String cleanupPolicy = input.getTopic().getProperties().get("cleanup.policy"); - - assertThat(cleanupPolicy).isEqualTo("compact"); - - Binder globalKTableBinder = binderFactory - .getBinder("globalktable", GlobalKTable.class); - - KafkaStreamsConsumerProperties inputX = (KafkaStreamsConsumerProperties) ((ExtendedPropertiesBinder) globalKTableBinder) - .getExtendedConsumerProperties("process-in-1"); - String cleanupPolicyX = inputX.getTopic().getProperties().get("cleanup.policy"); - - assertThat(cleanupPolicyX).isEqualTo("compact"); - - KafkaStreamsConsumerProperties inputY = (KafkaStreamsConsumerProperties) ((ExtendedPropertiesBinder) globalKTableBinder) - .getExtendedConsumerProperties("process-in-2"); - String cleanupPolicyY = inputY.getTopic().getProperties().get("cleanup.policy"); - - assertThat(cleanupPolicyY).isEqualTo("compact"); - - - Map senderPropsCustomer = KafkaTestUtils.producerProps(embeddedKafka); - senderPropsCustomer.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class); - senderPropsCustomer.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, - JsonSerializer.class); - - DefaultKafkaProducerFactory pfCustomer = - new DefaultKafkaProducerFactory<>(senderPropsCustomer); - KafkaTemplate template = new KafkaTemplate<>(pfCustomer, true); - template.setDefaultTopic("customers"); - for (long i = 0; i < 5; i++) { - final Customer customer = new Customer(); - customer.setName("customer-" + i); - template.sendDefault(i, customer); - } - - Map senderPropsProduct = KafkaTestUtils.producerProps(embeddedKafka); - senderPropsProduct.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class); - senderPropsProduct.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); - - DefaultKafkaProducerFactory pfProduct = - new DefaultKafkaProducerFactory<>(senderPropsProduct); - KafkaTemplate productTemplate = new KafkaTemplate<>(pfProduct, true); - productTemplate.setDefaultTopic("products"); - - for (long i = 0; i < 5; i++) { - final Product product = new Product(); - product.setName("product-" + i); - productTemplate.sendDefault(i, product); - } - - Map senderPropsOrder = KafkaTestUtils.producerProps(embeddedKafka); - senderPropsOrder.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class); - senderPropsOrder.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); - - DefaultKafkaProducerFactory pfOrder = new DefaultKafkaProducerFactory<>(senderPropsOrder); - KafkaTemplate orderTemplate = new KafkaTemplate<>(pfOrder, true); - orderTemplate.setDefaultTopic("orders"); - - for (long i = 0; i < 5; i++) { - final Order order = new Order(); - order.setCustomerId(i); - order.setProductId(i); - orderTemplate.sendDefault(i, order); - } - - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - JsonDeserializer.class); - consumerProps.put(JsonDeserializer.VALUE_DEFAULT_TYPE, - "org.springframework.cloud.stream.binder.kafka.streams." + - "function.StreamToGlobalKTableFunctionTests.EnrichedOrder"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "enriched-order"); - - int count = 0; - long start = System.currentTimeMillis(); - List> enrichedOrders = new ArrayList<>(); - do { - ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - count = count + records.count(); - for (ConsumerRecord record : records) { - enrichedOrders.add(new KeyValue<>(record.key(), record.value())); - } - } while (count < 5 && (System.currentTimeMillis() - start) < 30000); - - assertThat(count == 5).isTrue(); - assertThat(enrichedOrders.size() == 5).isTrue(); - - enrichedOrders.sort(Comparator.comparing(o -> o.key)); - - for (int i = 0; i < 5; i++) { - KeyValue enrichedOrderKeyValue = enrichedOrders.get(i); - assertThat(enrichedOrderKeyValue.key == i).isTrue(); - EnrichedOrder enrichedOrder = enrichedOrderKeyValue.value; - assertThat(enrichedOrder.getOrder().customerId == i).isTrue(); - assertThat(enrichedOrder.getOrder().productId == i).isTrue(); - assertThat(enrichedOrder.getCustomer().name.equals("customer-" + i)).isTrue(); - assertThat(enrichedOrder.getProduct().name.equals("product-" + i)).isTrue(); - } - pfCustomer.destroy(); - pfProduct.destroy(); - pfOrder.destroy(); - consumer.close(); - } - } - - @Test - void testTimeExtractor() throws Exception { - SpringApplication app = new SpringApplication(OrderEnricherApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=forTimeExtractorTest", - "--spring.cloud.stream.bindings.forTimeExtractorTest-in-0.destination=orders", - "--spring.cloud.stream.bindings.forTimeExtractorTest-in-1.destination=customers", - "--spring.cloud.stream.bindings.forTimeExtractorTest-in-2.destination=products", - "--spring.cloud.stream.bindings.forTimeExtractorTest-out-0.destination=enriched-order", - "--spring.cloud.stream.kafka.streams.bindings.forTimeExtractorTest-in-0.consumer.timestampExtractorBeanName" + - "=timestampExtractor", - "--spring.cloud.stream.kafka.streams.bindings.forTimeExtractorTest-in-1.consumer.timestampExtractorBeanName" + - "=timestampExtractor", - "--spring.cloud.stream.kafka.streams.bindings.forTimeExtractorTest-in-2.consumer.timestampExtractorBeanName" + - "=timestampExtractor", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=10000", - "--spring.cloud.stream.kafka.streams.bindings.order.consumer.applicationId=" + - "testTimeExtractor-abc", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - final KafkaStreamsExtendedBindingProperties kafkaStreamsExtendedBindingProperties = - context.getBean(KafkaStreamsExtendedBindingProperties.class); - - final Map bindings = kafkaStreamsExtendedBindingProperties.getBindings(); - - final KafkaStreamsBindingProperties kafkaStreamsBindingProperties0 = bindings.get("forTimeExtractorTest-in-0"); - final String timestampExtractorBeanName0 = kafkaStreamsBindingProperties0.getConsumer().getTimestampExtractorBeanName(); - final TimestampExtractor timestampExtractor0 = context.getBean(timestampExtractorBeanName0, TimestampExtractor.class); - assertThat(timestampExtractor0).isNotNull(); - - final KafkaStreamsBindingProperties kafkaStreamsBindingProperties1 = bindings.get("forTimeExtractorTest-in-1"); - final String timestampExtractorBeanName1 = kafkaStreamsBindingProperties1.getConsumer().getTimestampExtractorBeanName(); - final TimestampExtractor timestampExtractor1 = context.getBean(timestampExtractorBeanName1, TimestampExtractor.class); - assertThat(timestampExtractor1).isNotNull(); - - final KafkaStreamsBindingProperties kafkaStreamsBindingProperties2 = bindings.get("forTimeExtractorTest-in-2"); - final String timestampExtractorBeanName2 = kafkaStreamsBindingProperties2.getConsumer().getTimestampExtractorBeanName(); - final TimestampExtractor timestampExtractor2 = context.getBean(timestampExtractorBeanName2, TimestampExtractor.class); - assertThat(timestampExtractor2).isNotNull(); - } - } - - @EnableAutoConfiguration - public static class OrderEnricherApplication { - - @Bean - public Function, - Function, - Function, KStream>>> process() { - - return orderStream -> ( - customers -> ( - products -> ( - orderStream.join(customers, - (orderId, order) -> order.getCustomerId(), - (order, customer) -> new CustomerOrder(customer, order)) - .join(products, - (orderId, customerOrder) -> customerOrder - .productId(), - (customerOrder, product) -> { - EnrichedOrder enrichedOrder = new EnrichedOrder(); - enrichedOrder.setProduct(product); - enrichedOrder.setCustomer(customerOrder.customer); - enrichedOrder.setOrder(customerOrder.order); - return enrichedOrder; - }) - ) - ) - ); - } - - @Bean - public Function, - Function, - Function, KStream>>> forTimeExtractorTest() { - return orderStream -> - customers -> - products -> orderStream; - } - - @Bean - public TimestampExtractor timestampExtractor() { - return new WallclockTimestampExtractor(); - } - } - - static class Order { - - long customerId; - long productId; - - public long getCustomerId() { - return customerId; - } - - public void setCustomerId(long customerId) { - this.customerId = customerId; - } - - public long getProductId() { - return productId; - } - - public void setProductId(long productId) { - this.productId = productId; - } - } - - static class Customer { - - String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - static class Product { - - String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - static class EnrichedOrder { - - Product product; - Customer customer; - Order order; - - public Product getProduct() { - return product; - } - - public void setProduct(Product product) { - this.product = product; - } - - public Customer getCustomer() { - return customer; - } - - public void setCustomer(Customer customer) { - this.customer = customer; - } - - public Order getOrder() { - return order; - } - - public void setOrder(Order order) { - this.order = order; - } - } - - private static class CustomerOrder { - private final Customer customer; - private final Order order; - - CustomerOrder(final Customer customer, final Order order) { - this.customer = customer; - this.order = order; - } - - long productId() { - return order.getProductId(); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/StreamToTableJoinFunctionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/StreamToTableJoinFunctionTests.java deleted file mode 100644 index 6fba778d8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/function/StreamToTableJoinFunctionTests.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.function; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.serialization.LongDeserializer; -import org.apache.kafka.common.serialization.LongSerializer; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.JoinWindows; -import org.apache.kafka.streams.kstream.Joined; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.KTable; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.StreamJoined; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.streams.properties.KafkaStreamsProducerProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.util.Assert; - -import static org.assertj.core.api.Assertions.assertThat; - -@EmbeddedKafka(topics = "output-topic-1") -public class StreamToTableJoinFunctionTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void testStreamToTable() { - SpringApplication app = new SpringApplication(CountClicksPerRegionApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - Consumer consumer; - Map consumerProps = KafkaTestUtils.consumerProps("group-1", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "output-topic-1"); - - runTest(app, consumer); - } - - @Test - void testStreamToTableBiFunction() { - SpringApplication app = new SpringApplication(BiFunctionCountClicksPerRegionApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - Consumer consumer; - Map consumerProps = KafkaTestUtils.consumerProps("group-2", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "output-topic-1"); - - runTest(app, consumer); - } - - @Test - void testStreamToTableBiConsumer() throws Exception { - SpringApplication app = new SpringApplication(BiConsumerApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - Consumer consumer; - Map consumerProps = KafkaTestUtils.consumerProps("group-2", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "output-topic-1"); - - try (ConfigurableApplicationContext ignored = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=user-clicks-1", - "--spring.cloud.stream.bindings.process-in-1.destination=user-regions-1", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=10000", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.applicationId" + - "=testStreamToTableBiConsumer", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - // Input 1: Region per user (multiple records allowed per user). - List> userRegions = Arrays.asList( - new KeyValue<>("alice", "asia") - ); - - Map senderProps1 = KafkaTestUtils.producerProps(embeddedKafka); - senderProps1.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - senderProps1.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - - DefaultKafkaProducerFactory pf1 = new DefaultKafkaProducerFactory<>(senderProps1); - KafkaTemplate template1 = new KafkaTemplate<>(pf1, true); - template1.setDefaultTopic("user-regions-1"); - - for (KeyValue keyValue : userRegions) { - template1.sendDefault(keyValue.key, keyValue.value); - } - - // Input 2: Clicks per user (multiple records allowed per user). - List> userClicks = Arrays.asList( - new KeyValue<>("alice", 13L) - ); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, LongSerializer.class); - - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("user-clicks-1"); - - for (KeyValue keyValue : userClicks) { - template.sendDefault(keyValue.key, keyValue.value); - } - - Assert.isTrue(BiConsumerApplication.latch.await(10, TimeUnit.SECONDS), "Failed to receive message"); - - } - finally { - consumer.close(); - } - } - - private void runTest(SpringApplication app, Consumer consumer) { - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=user-clicks-1", - "--spring.cloud.stream.bindings.process-in-1.destination=user-regions-1", - "--spring.cloud.stream.bindings.process-out-0.destination=output-topic-1", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=10000", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.applicationId" + - "=StreamToTableJoinFunctionTests-abc", - "--spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.topic.properties.cleanup.policy=compact", - "--spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.topic.properties.cleanup.policy=compact", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - // Input 1: Region per user (multiple records allowed per user). - List> userRegions = Arrays.asList( - new KeyValue<>("alice", "asia"), /* Alice lived in Asia originally... */ - new KeyValue<>("bob", "americas"), - new KeyValue<>("chao", "asia"), - new KeyValue<>("dave", "europe"), - new KeyValue<>("alice", "europe"), /* ...but moved to Europe some time later. */ - new KeyValue<>("eve", "americas"), - new KeyValue<>("fang", "asia") - ); - - Map senderProps1 = KafkaTestUtils.producerProps(embeddedKafka); - senderProps1.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - senderProps1.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - - DefaultKafkaProducerFactory pf1 = new DefaultKafkaProducerFactory<>(senderProps1); - KafkaTemplate template1 = new KafkaTemplate<>(pf1, true); - template1.setDefaultTopic("user-regions-1"); - - for (KeyValue keyValue : userRegions) { - template1.sendDefault(keyValue.key, keyValue.value); - } - - // Input 2: Clicks per user (multiple records allowed per user). - List> userClicks = Arrays.asList( - new KeyValue<>("alice", 13L), - new KeyValue<>("bob", 4L), - new KeyValue<>("chao", 25L), - new KeyValue<>("bob", 19L), - new KeyValue<>("dave", 56L), - new KeyValue<>("eve", 78L), - new KeyValue<>("alice", 40L), - new KeyValue<>("fang", 99L) - ); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, LongSerializer.class); - - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("user-clicks-1"); - - for (KeyValue keyValue : userClicks) { - template.sendDefault(keyValue.key, keyValue.value); - } - - List> expectedClicksPerRegion = Arrays.asList( - new KeyValue<>("americas", 101L), - new KeyValue<>("europe", 109L), - new KeyValue<>("asia", 124L) - ); - - //Verify that we receive the expected data - int count = 0; - long start = System.currentTimeMillis(); - List> actualClicksPerRegion = new ArrayList<>(); - do { - ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - count = count + records.count(); - for (ConsumerRecord record : records) { - actualClicksPerRegion.add(new KeyValue<>(record.key(), record.value())); - } - } while (count < expectedClicksPerRegion.size() && (System.currentTimeMillis() - start) < 30000); - - assertThat(count == expectedClicksPerRegion.size()).isTrue(); - assertThat(actualClicksPerRegion).hasSameElementsAs(expectedClicksPerRegion); - - // Testing certain ancillary configuration of GlobalKTable around topics creation. - // See this issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/687 - BinderFactory binderFactory = context.getBeanFactory() - .getBean(BinderFactory.class); - - Binder ktableBinder = binderFactory - .getBinder("ktable", KTable.class); - - KafkaStreamsConsumerProperties inputX = (KafkaStreamsConsumerProperties) ((ExtendedPropertiesBinder) ktableBinder) - .getExtendedConsumerProperties("process-in-1"); - String cleanupPolicyX = inputX.getTopic().getProperties().get("cleanup.policy"); - - assertThat(cleanupPolicyX).isEqualTo("compact"); - - Binder kStreamBinder = binderFactory - .getBinder("kstream", KStream.class); - - KafkaStreamsProducerProperties producerProperties = (KafkaStreamsProducerProperties) ((ExtendedPropertiesBinder) kStreamBinder) - .getExtendedProducerProperties("process-out-0"); - - String cleanupPolicyOutput = producerProperties.getTopic().getProperties().get("cleanup.policy"); - - assertThat(cleanupPolicyOutput).isEqualTo("compact"); - } - finally { - consumer.close(); - } - } - -// @Test - public void testGlobalStartOffsetWithLatestAndIndividualBindingWthEarliest() throws Exception { - SpringApplication app = new SpringApplication(BiFunctionCountClicksPerRegionApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - Consumer consumer; - Map consumerProps = KafkaTestUtils.consumerProps("group-3", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "output-topic-2"); - - // Produce data first to the input topic to test the startOffset setting on the - // binding (which is set to earliest below). - // Input 1: Clicks per user (multiple records allowed per user). - List> userClicks = Arrays.asList( - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L), - new KeyValue<>("alice", 100L) - ); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, LongSerializer.class); - - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("user-clicks-2"); - - for (KeyValue keyValue : userClicks) { - template.sendDefault(keyValue.key, keyValue.value); - } - - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=user-clicks-2", - "--spring.cloud.stream.bindings.process-in-1.destination=user-regions-2", - "--spring.cloud.stream.bindings.process-out-0.destination=output-topic-2", - "--spring.cloud.stream.bindings.process-in-0.consumer.useNativeDecoding=true", - "--spring.cloud.stream.bindings.process-in-1.consumer.useNativeDecoding=true", - "--spring.cloud.stream.bindings.process-out-0.producer.useNativeEncoding=true", - "--spring.cloud.stream.kafka.streams.binder.configuration.auto.offset.reset=latest", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.startOffset=earliest", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" + - "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=10000", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.application-id" + - "=StreamToTableJoinFunctionTests-foobar", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.kafka.streams.binder.zkNodes=" + embeddedKafka.getZookeeperConnectionString())) { - Thread.sleep(1000L); - - // Input 2: Region per user (multiple records allowed per user). - List> userRegions = Arrays.asList( - new KeyValue<>("alice", "asia"), /* Alice lived in Asia originally... */ - new KeyValue<>("bob", "americas"), - new KeyValue<>("chao", "asia"), - new KeyValue<>("dave", "europe"), - new KeyValue<>("alice", "europe"), /* ...but moved to Europe some time later. */ - new KeyValue<>("eve", "americas"), - new KeyValue<>("fang", "asia") - ); - - Map senderProps1 = KafkaTestUtils.producerProps(embeddedKafka); - senderProps1.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - senderProps1.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - - DefaultKafkaProducerFactory pf1 = new DefaultKafkaProducerFactory<>(senderProps1); - KafkaTemplate template1 = new KafkaTemplate<>(pf1, true); - template1.setDefaultTopic("user-regions-2"); - - for (KeyValue keyValue : userRegions) { - template1.sendDefault(keyValue.key, keyValue.value); - } - - - // Input 1: Clicks per user (multiple records allowed per user). - List> userClicks1 = Arrays.asList( - new KeyValue<>("bob", 4L), - new KeyValue<>("chao", 25L), - new KeyValue<>("bob", 19L), - new KeyValue<>("dave", 56L), - new KeyValue<>("eve", 78L), - new KeyValue<>("fang", 99L) - ); - - for (KeyValue keyValue : userClicks1) { - template.sendDefault(keyValue.key, keyValue.value); - } - - List> expectedClicksPerRegion = Arrays.asList( - new KeyValue<>("americas", 101L), - new KeyValue<>("europe", 56L), - new KeyValue<>("asia", 124L), - //1000 alice entries which were there in the topic before the consumer started. - //Since we set the startOffset to earliest for the topic, it will read them, - //but the join fails to associate with a valid region, thus UNKNOWN. - new KeyValue<>("UNKNOWN", 1000L) - ); - - //Verify that we receive the expected data - int count = 0; - long start = System.currentTimeMillis(); - List> actualClicksPerRegion = new ArrayList<>(); - do { - ConsumerRecords records = KafkaTestUtils.getRecords(consumer); - count = count + records.count(); - for (ConsumerRecord record : records) { - actualClicksPerRegion.add(new KeyValue<>(record.key(), record.value())); - } - } while (count < expectedClicksPerRegion.size() && (System.currentTimeMillis() - start) < 30000); - - // TODO: Matched count is 3 and not 4 (expectedClicksPerRegion.size()) when running with full suite. Investigate why. - // TODO: This behavior is only observed after the Spring Kafka upgrade to 2.5.0 and kafka client to 2.5. - // TODO: Note that the test passes fine as a single test. - assertThat(count).matches( - matchedCount -> matchedCount == expectedClicksPerRegion.size() - 1 || matchedCount == expectedClicksPerRegion.size()); - assertThat(actualClicksPerRegion).containsAnyElementsOf(expectedClicksPerRegion); - } - finally { - consumer.close(); - } - } - - @Test - public void testTrivialSingleKTableInputAsNonDeclarative() { - SpringApplication app = new SpringApplication(TrivialKTableApp.class); - app.setWebApplicationType(WebApplicationType.NONE); - app.run("--server.port=0", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.application-id=" + - "testTrivialSingleKTableInputAsNonDeclarative"); - //All we are verifying is that this application didn't throw any errors. - //See this issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/536 - } - - @Test - public void testTwoKStreamsCanBeJoined() { - SpringApplication app = new SpringApplication( - JoinProcessor.class); - app.setWebApplicationType(WebApplicationType.NONE); - app.run("--server.port=0", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString(), - "--spring.application.name=" + - "two-kstream-input-join-integ-test"); - //All we are verifying is that this application didn't throw any errors. - //See this issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/701 - } - - - /** - * Tuple for a region and its associated number of clicks. - */ - private static final class RegionWithClicks { - - private final String region; - private final long clicks; - - RegionWithClicks(String region, long clicks) { - if (region == null || region.isEmpty()) { - throw new IllegalArgumentException("region must be set"); - } - if (clicks < 0) { - throw new IllegalArgumentException("clicks must not be negative"); - } - this.region = region; - this.clicks = clicks; - } - - public String getRegion() { - return region; - } - - public long getClicks() { - return clicks; - } - - } - - @EnableAutoConfiguration - public static class CountClicksPerRegionApplication { - - @Bean - public Function, Function, KStream>> process() { - return userClicksStream -> (userRegionsTable -> (userClicksStream - .leftJoin(userRegionsTable, (clicks, region) -> new RegionWithClicks(region == null ? - "UNKNOWN" : region, clicks), - Joined.with(Serdes.String(), Serdes.Long(), null)) - .map((user, regionWithClicks) -> new KeyValue<>(regionWithClicks.getRegion(), - regionWithClicks.getClicks())) - .groupByKey(Grouped.with(Serdes.String(), Serdes.Long())) - .reduce(Long::sum, Materialized.as("CountClicks-" + UUID.randomUUID())) - .toStream())); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - } - - @EnableAutoConfiguration - public static class BiFunctionCountClicksPerRegionApplication { - - @Bean - public BiFunction, KTable, KStream> process() { - return (userClicksStream, userRegionsTable) -> (userClicksStream - .leftJoin(userRegionsTable, (clicks, region) -> new RegionWithClicks(region == null ? - "UNKNOWN" : region, clicks), - Joined.with(Serdes.String(), Serdes.Long(), null)) - .map((user, regionWithClicks) -> new KeyValue<>(regionWithClicks.getRegion(), - regionWithClicks.getClicks())) - .groupByKey(Grouped.with(Serdes.String(), Serdes.Long())) - .reduce(Long::sum, Materialized.as("CountClicks-" + UUID.randomUUID())) - .toStream()); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - } - - @EnableAutoConfiguration - public static class BiConsumerApplication { - - static CountDownLatch latch = new CountDownLatch(2); - - @Bean - public BiConsumer, KTable> process() { - return (userClicksStream, userRegionsTable) -> { - userClicksStream.foreach((key, value) -> latch.countDown()); - userRegionsTable.toStream().foreach((key, value) -> latch.countDown()); - }; - } - } - - @EnableAutoConfiguration - public static class TrivialKTableApp { - - public java.util.function.Consumer> process() { - return inputTable -> inputTable.toStream().foreach((key, value) -> System.out.println("key : value " + key + " : " + value)); - } - } - - @EnableAutoConfiguration - public static class JoinProcessor { - - public BiConsumer, KStream> testProcessor() { - return (input1Stream, input2Stream) -> input1Stream - .join(input2Stream, - (event1, event2) -> null, - JoinWindows.of(Duration.ofMillis(5)), - StreamJoined.with( - Serdes.String(), - Serdes.String(), - Serdes.String() - ) - ); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/DlqDestinationResolverTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/DlqDestinationResolverTests.java deleted file mode 100644 index 5b916ecc0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/DlqDestinationResolverTests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.streams.integration; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBindingInformationCatalogue; -import org.springframework.cloud.stream.binder.kafka.utils.DlqDestinationResolver; -import org.springframework.cloud.stream.binder.kafka.utils.DlqPartitionFunction; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(topics = {"topic1-dlq", "topic2-dlq"}) -public class DlqDestinationResolverTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void testDlqDestinationResolverWorks() throws Exception { - SpringApplication app = new SpringApplication(WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.process-in-0.destination=word1,word2", - "--spring.cloud.stream.bindings.process-out-0.destination=test-output", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.application-id=dlq-dest-resolver-test", - "--spring.cloud.stream.kafka.streams.binder.serdeError=sendToDlq", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.valueSerde=" - + "org.apache.kafka.common.serialization.Serdes$IntegerSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("word1"); - template.sendDefault("foobar"); - - template.setDefaultTopic("word2"); - template.sendDefault("foobar"); - - Map consumerProps = KafkaTestUtils.consumerProps("some-random-group", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - Consumer consumer1 = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer1, "topic1-dlq", - "topic2-dlq"); - - ConsumerRecord cr1 = KafkaTestUtils.getSingleRecord(consumer1, - "topic1-dlq"); - assertThat(cr1.value()).isEqualTo("foobar"); - ConsumerRecord cr2 = KafkaTestUtils.getSingleRecord(consumer1, - "topic2-dlq"); - assertThat(cr2.value()).isEqualTo("foobar"); - - final KafkaStreamsBindingInformationCatalogue catalogue = context.getBean(KafkaStreamsBindingInformationCatalogue.class); - assertThat(catalogue.getDlqProducerFactories().size()).isEqualTo(1); - } - finally { - pf.destroy(); - } - } - } - - @EnableAutoConfiguration - public static class WordCountProcessorApplication { - - @Bean - public Function, KStream> process() { - - return input -> input - .flatMapValues( - value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(Serdes.String(), Serdes.String())) - .windowedBy(TimeWindows.of(Duration.ofSeconds(5))).count(Materialized.as("foo-WordCounts-x")) - .toStream().map((key, value) -> new KeyValue<>(null, - "Count for " + key.key() + " : " + value)); - } - - @Bean - public DlqPartitionFunction partitionFunction() { - return (group, rec, ex) -> 0; - } - - @Bean - public DlqDestinationResolver dlqDestinationResolver() { - return (rec, ex) -> { - if (rec.topic().equals("word1")) { - return "topic1-dlq"; - } - return "topic2-dlq"; - }; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/DltAwareProcessorTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/DltAwareProcessorTests.java deleted file mode 100644 index 744cf9f9f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/DltAwareProcessorTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.streams.integration; - -import java.util.Map; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.kafka.streams.DltAwareProcessor; -import org.springframework.cloud.stream.binder.kafka.streams.DltPublishingContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(topics = "hello-dlt-1") -public class DltAwareProcessorTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "hello-dlt-1"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testDltAwareProcessor() { - SpringApplication app = new SpringApplication( - DltAwareProcessorTests.PublishToDltOnErrorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.cloud.stream.bindings.errorStream-in-0.destination=error-stream-in", - "--spring.cloud.stream.kafka.streams.bindings.errorStream-in-0.consumer.application-id=test-error-stream-app-id", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString())) { - receiveAndValidate("error-stream-in", "hello-dlt-1"); - } - } - - private void receiveAndValidate(String in, String out) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic(in); - template.sendDefault("foobar"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - out); - assertThat(cr.value().contains("foobar")).isTrue(); - } - finally { - pf.destroy(); - } - } - - @EnableAutoConfiguration - static class PublishToDltOnErrorApplication { - - @Bean - public java.util.function.Consumer> errorStream(DltPublishingContext dltSenderContext) { - return input -> input - .process(() -> new DltAwareProcessor<>((k, v) -> { - throw new RuntimeException("error"); - }, "hello-dlt-1", dltSenderContext)); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderDestinationIsPatternTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderDestinationIsPatternTests.java deleted file mode 100644 index 9ea6a9cdc..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderDestinationIsPatternTests.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.integration; - -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.streams.kstream.KStream; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Michael Stoettinger - */ -@EmbeddedKafka(topics = {"in.1", "in.2", "out"}) -public class KafkaStreamsBinderDestinationIsPatternTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static org.apache.kafka.clients.consumer.Consumer consumer; - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("group", "true", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "out"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void test() { - SpringApplication app = new SpringApplication(ConsumingApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.cloud.stream.bindings.process-out-0.destination=out", - "--spring.cloud.stream.bindings.process-in-0.destination=in.*", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.destinationIsPattern=true", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(producerFactory, true); - - // send message to both topics that fit the pattern - template.send("in.1", "foo1"); - assertThat(KafkaTestUtils.getSingleRecord(consumer, "out").value()) - .isEqualTo("foo1"); - template.send("in.2", "foo2"); - assertThat(KafkaTestUtils.getSingleRecord(consumer, "out").value()) - .isEqualTo("foo2"); - } - finally { - context.close(); - } - } - - @EnableAutoConfiguration - public static class ConsumingApplication { - - @Bean - public Function, KStream> process() { - return input -> input; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderHealthIndicatorTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderHealthIndicatorTests.java deleted file mode 100644 index 6f30eda93..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderHealthIndicatorTests.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.integration; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.errors.StreamsUncaughtExceptionHandler; -import org.apache.kafka.streams.kstream.KStream; -import org.assertj.core.util.Lists; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.actuate.health.CompositeHealthContributor; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsBinderHealthIndicator; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.config.StreamsBuilderFactoryBeanConfigurer; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.SendResult; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Arnaud JardinΓ© - * @author Chris Bono - */ -@EmbeddedKafka(topics = {"out", "out2"}) -public class KafkaStreamsBinderHealthIndicatorTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @BeforeAll - public static void setUp() { - System.setProperty("logging.level.org.apache.kafka", "OFF"); - } - - @Test - void healthIndicatorUpTest() throws Exception { - try (ConfigurableApplicationContext context = singleStream("ApplicationHealthTest-xyz")) { - receive(context, - Lists.newArrayList(new ProducerRecord<>("in", "{\"id\":\"123\"}"), - new ProducerRecord<>("in", "{\"id\":\"123\"}")), - Status.UP, "out"); - } - } - - @Test - void healthIndicatorUpMultipleCallsTest() throws Exception { - try (ConfigurableApplicationContext context = singleStream("ApplicationHealthTest-xyz")) { - int callsToPerform = 5; - for (int i = 0; i < callsToPerform; i++) { - receive(context, - Lists.newArrayList(new ProducerRecord<>("in", "{\"id\":\"123\"}"), - new ProducerRecord<>("in", "{\"id\":\"123\"}")), - Status.UP, "out"); - } - } - } - - @Test - public void healthIndicatorDownTest() throws Exception { - try (ConfigurableApplicationContext context = singleStream("ApplicationHealthTest-xyzabc")) { - receive(context, - Lists.newArrayList(new ProducerRecord<>("in", "{\"id\":\"123\"}"), - new ProducerRecord<>("in", "{\"id\":\"124\"}")), - Status.DOWN, "out"); - } - } - - @Test - public void healthIndicatorUpMultipleKStreamsTest() throws Exception { - try (ConfigurableApplicationContext context = multipleStream()) { - receive(context, - Lists.newArrayList(new ProducerRecord<>("in", "{\"id\":\"123\"}"), - new ProducerRecord<>("in2", "{\"id\":\"123\"}")), - Status.UP, "out", "out2"); - } - } - - @Test - public void healthIndicatorDownMultipleKStreamsTest() throws Exception { - try (ConfigurableApplicationContext context = multipleStream()) { - receive(context, - Lists.newArrayList(new ProducerRecord<>("in", "{\"id\":\"123\"}"), - new ProducerRecord<>("in2", "{\"id\":\"124\"}")), - Status.DOWN, "out", "out2"); - } - } - - private static boolean waitFor(Status status, Map details) { - if (status == Status.UP) { - String threadState = (String) details.get("threadState"); - return threadState != null - && (threadState.equalsIgnoreCase(KafkaStreams.State.REBALANCING.name()) - || threadState.equalsIgnoreCase("PARTITIONS_REVOKED") - || threadState.equalsIgnoreCase("PARTITIONS_ASSIGNED") - || threadState.equalsIgnoreCase( - KafkaStreams.State.PENDING_SHUTDOWN.name())); - } - return false; - } - - private void receive(ConfigurableApplicationContext context, - List> records, Status expected, - String... topics) throws Exception { - Map consumerProps = KafkaTestUtils.consumerProps("group-id0", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - try (Consumer consumer = cf.createConsumer()) { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - CountDownLatch latch = new CountDownLatch(records.size()); - for (ProducerRecord record : records) { - CompletableFuture> future = template.send(record); - future.whenComplete((result, ex) -> { - if (ex != null) { - Assertions.fail(); - } - else { - latch.countDown(); - } - }); - } - latch.await(5, TimeUnit.SECONDS); - - embeddedKafka.consumeFromEmbeddedTopics(consumer, topics); - KafkaTestUtils.getRecords(consumer, Duration.ofSeconds(1)); - - TimeUnit.SECONDS.sleep(5); - checkHealth(context, expected); - } - finally { - pf.destroy(); - } - } - - private static void checkHealth(ConfigurableApplicationContext context, - Status expected) throws InterruptedException { - CompositeHealthContributor healthIndicator = context - .getBean("bindersHealthContributor", CompositeHealthContributor.class); - KafkaStreamsBinderHealthIndicator kafkaStreamsBinderHealthIndicator = (KafkaStreamsBinderHealthIndicator) healthIndicator.getContributor("kstream"); - Health health = kafkaStreamsBinderHealthIndicator.health(); - while (waitFor(health.getStatus(), health.getDetails())) { - TimeUnit.SECONDS.sleep(2); - health = kafkaStreamsBinderHealthIndicator.health(); - } - assertThat(health.getStatus()).isEqualTo(expected); - } - - private ConfigurableApplicationContext singleStream(String applicationId) { - SpringApplication app = new SpringApplication(KStreamApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - return app.run("--server.port=0", "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.bindings.input.destination=in", - "--spring.cloud.stream.bindings.output.destination=out", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=" - + "org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=" - + "org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId=" - + applicationId, - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - } - - private ConfigurableApplicationContext multipleStream() { - System.setProperty("logging.level.org.apache.kafka", "OFF"); - SpringApplication app = new SpringApplication(AnotherKStreamApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - return app.run("--server.port=0", "--spring.jmx.enabled=false", - "--spring.cloud.function.definition=process;process2", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.function.bindings.process2-in-0=input2", - "--spring.cloud.stream.function.bindings.process2-out-0=output2", - "--spring.cloud.stream.bindings.input.destination=in", - "--spring.cloud.stream.bindings.output.destination=out", - "--spring.cloud.stream.bindings.input2.destination=in2", - "--spring.cloud.stream.bindings.output2.destination=out2", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=" - + "org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=" - + "org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId=" - + "ApplicationHealthTest-xyz", - "--spring.cloud.stream.kafka.streams.bindings.input2.consumer.applicationId=" - + "ApplicationHealthTest2-xyz", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - } - - @EnableAutoConfiguration - public static class KStreamApplication { - - @Bean - public Function, KStream> process() { - return input -> input.filter((key, product) -> { - if (product.getId() != 123) { - throw new IllegalArgumentException(); - } - return true; - }); - } - - } - - @EnableAutoConfiguration - public static class AnotherKStreamApplication { - - @Bean - public Function, KStream> process() { - return input -> input.filter((key, product) -> { - if (product.getId() != 123) { - throw new IllegalArgumentException(); - } - return true; - }); - } - - @Bean - public Function, KStream> process2() { - return input -> input.filter((key, product) -> { - if (product.getId() != 123) { - throw new IllegalArgumentException(); - } - return true; - }); - } - - @Bean - public StreamsBuilderFactoryBeanConfigurer customizer() { - return factoryBean -> { - factoryBean.setKafkaStreamsCustomizer(kafkaStreams -> kafkaStreams.setUncaughtExceptionHandler(exception -> - StreamsUncaughtExceptionHandler.StreamThreadExceptionResponse.SHUTDOWN_CLIENT)); - }; - } - - } - - public static class Product { - - Integer id; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderMultipleInputTopicsTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderMultipleInputTopicsTest.java deleted file mode 100644 index 6ea7ff7e5..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderMultipleInputTopicsTest.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams.integration; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This test case demonstrates a kafk-streams topology which consumes messages from - * multiple kafka topics(destinations). - * - * See - * {@link KafkaStreamsBinderMultipleInputTopicsTest#testKstreamWordCountWithStringInputAndPojoOuput} - * where the input topic names are specified as comma-separated String values for the - * property spring.cloud.stream.bindings.input.destination. - * - * @author Sarath Shyam - */ -@EmbeddedKafka(topics = "counts") -public class KafkaStreamsBinderMultipleInputTopicsTest { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() throws Exception { - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "counts"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testKstreamWordCountWithStringInputAndPojoOuput() throws Exception { - SpringApplication app = new SpringApplication( - WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.bindings.input.destination=words1,words2", - "--spring.cloud.stream.bindings.output.destination=counts", - "--spring.cloud.stream.bindings.output.contentType=application/json", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.timeWindow.length=5000", - "--spring.cloud.stream.kafka.streams.timeWindow.advanceBy=0", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId" - + "=WordCountProcessorApplication-xyz", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - receiveAndValidate(); - } - finally { - context.close(); - } - } - - private void receiveAndValidate() - throws Exception { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words1"); - template.sendDefault("foobar1"); - template.setDefaultTopic("words2"); - template.sendDefault("foobar2"); - // Sleep a bit so that both the messages are processed before reading from the - // output topic. - // Else assertions might fail arbitrarily. - Thread.sleep(5000); - ConsumerRecords received = KafkaTestUtils.getRecords(consumer); - List wordCounts = new ArrayList<>(2); - - received.records("counts") - .forEach((consumerRecord) -> wordCounts.add((consumerRecord.value()))); - System.out.println(wordCounts); - assertThat(wordCounts.contains("{\"word\":\"foobar1\",\"count\":1}")).isTrue(); - assertThat(wordCounts.contains("{\"word\":\"foobar2\",\"count\":1}")).isTrue(); - } - - @EnableAutoConfiguration - static class WordCountProcessorApplication { - - @Bean - public Function, KStream> process() { - - return input -> input - .flatMapValues( - value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(Serdes.String(), Serdes.String())) - .count(Materialized.as("WordCounts-tKWCWSIAP0")).toStream() - .map((key, value) -> new KeyValue<>(null, new WordCount(key, value))); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(false, true); - } - - } - - static class WordCount { - - private String word; - - private long count; - - WordCount(String word, long count) { - this.word = word; - this.count = count; - } - - public String getWord() { - return word; - } - - public void setWord(String word) { - this.word = word; - } - - public long getCount() { - return count; - } - - public void setCount(long count) { - this.count = count; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderPojoInputAndPrimitiveTypeOutputTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderPojoInputAndPrimitiveTypeOutputTests.java deleted file mode 100644 index 7ac8bf127..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderPojoInputAndPrimitiveTypeOutputTests.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams.integration; - -import java.time.Duration; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.serialization.LongDeserializer; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.serializer.JsonSerde; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - * @author Gary Russell - */ -@EmbeddedKafka(topics = "counts-id") -public class KafkaStreamsBinderPojoInputAndPrimitiveTypeOutputTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() throws Exception { - Map consumerProps = KafkaTestUtils.consumerProps("group-id", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put("value.deserializer", LongDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "counts-id"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testKstreamBinderWithPojoInputAndStringOuput() throws Exception { - SpringApplication app = new SpringApplication(ProductCountApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.bindings.input.destination=foos", - "--spring.cloud.stream.bindings.output.destination=counts-id", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=" - + "org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=" - + "org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId=" - + "KafkaStreamsBinderPojoInputAndPrimitiveTypeOutputTests-xyz", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - receiveAndValidateFoo(); - } - finally { - context.close(); - } - } - - private void receiveAndValidateFoo() { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foos"); - template.sendDefault("{\"id\":\"123\"}"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - "counts-id"); - - assertThat(cr.key()).isEqualTo(123); - assertThat(cr.value()).isEqualTo(1L); - } - - @EnableAutoConfiguration - public static class ProductCountApplication { - - @Bean - public Function, KStream> process() { - return input -> input.filter((key, product) -> product.getId() == 123) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(new JsonSerde<>(Product.class), - new JsonSerde<>(Product.class))) - .windowedBy(TimeWindows.of(Duration.ofMillis(5000))) - .count(Materialized.as("id-count-store-x")).toStream() - .map((key, value) -> new KeyValue<>(key.key().id, value)); - } - } - - public static class Product { - - Integer id; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderTombstoneTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderTombstoneTests.java deleted file mode 100644 index 9b48d3dd6..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsBinderTombstoneTests.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams.integration; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Date; -import java.util.Map; -import java.util.Properties; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KafkaStreams; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.StreamsConfig; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Marius Bogoevici - * @author Soby Chacko - * @author Gary Russell - */ -@EmbeddedKafka(topics = "counts-1") -public class KafkaStreamsBinderTombstoneTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() { - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "counts-1"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testSendToTombstone() - throws Exception { - SpringApplication app = new SpringApplication( - WordCountProcessorApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - - try (ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.process-in-0.destination=words-1", - "--spring.cloud.stream.bindings.process-out-0.destination=counts-1", - "--spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.application-id=testKstreamWordCountWithInputBindingLevelApplicationId", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.valueSerde=org.springframework.kafka.support.serializer.JsonSerde", - "--spring.cloud.stream.bindings.process-in-0.consumer.concurrency=2", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString())) { - receiveAndValidate("words-1", "counts-1"); - // Assertions on StreamBuilderFactoryBean - StreamsBuilderFactoryBean streamsBuilderFactoryBean = context - .getBean("&stream-builder-process", StreamsBuilderFactoryBean.class); - KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); - assertThat(kafkaStreams).isNotNull(); - // Ensure that concurrency settings are mapped to number of stream task - // threads in Kafka Streams. - final Properties streamsConfiguration = streamsBuilderFactoryBean.getStreamsConfiguration(); - final Integer concurrency = (Integer) streamsConfiguration - .get(StreamsConfig.NUM_STREAM_THREADS_CONFIG); - assertThat(concurrency).isEqualTo(2); - - sendTombStoneRecordsAndVerifyGracefulHandling(); - - CleanupConfig cleanup = TestUtils.getPropertyValue(streamsBuilderFactoryBean, - "cleanupConfig", CleanupConfig.class); - assertThat(cleanup.cleanupOnStart()).isTrue(); - assertThat(cleanup.cleanupOnStop()).isFalse(); - } - } - - private void receiveAndValidate(String in, String out) { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic(in); - template.sendDefault("foobar"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - out); - assertThat(cr.value().contains("\"word\":\"foobar\",\"count\":1")).isTrue(); - } - finally { - pf.destroy(); - } - } - - private void sendTombStoneRecordsAndVerifyGracefulHandling() throws Exception { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - try { - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("words-1"); - template.sendDefault(null); - ConsumerRecords received = consumer - .poll(Duration.ofMillis(5000)); - // By asserting that the received record is empty, we are ensuring that the - // tombstone record - // was handled by the binder gracefully. - assertThat(received.isEmpty()).isTrue(); - } - finally { - pf.destroy(); - } - } - - @EnableAutoConfiguration - static class WordCountProcessorApplication { - - @Bean - public Function, KStream> process() { - - return input -> input - .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(Serdes.String(), Serdes.String())) - .windowedBy(TimeWindows.of(Duration.ofMillis(5000))) - .count(Materialized.as("foo-WordCounts")) - .toStream() - .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, - new Date(key.window().start()), new Date(key.window().end())))); - } - - @Bean - public CleanupConfig cleanupConfig() { - return new CleanupConfig(true, false); - } - - } - - static class WordCount { - - private String word; - - private long count; - - private Date start; - - private Date end; - - WordCount(String word, long count, Date start, Date end) { - this.word = word; - this.count = count; - this.start = start; - this.end = end; - } - - public String getWord() { - return word; - } - - public void setWord(String word) { - this.word = word; - } - - public long getCount() { - return count; - } - - public void setCount(long count) { - this.count = count; - } - - public Date getStart() { - return start; - } - - public void setStart(Date start) { - this.start = start; - } - - public Date getEnd() { - return end; - } - - public void setEnd(Date end) { - this.end = end; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsNativeEncodingDecodingTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsNativeEncodingDecodingTests.java deleted file mode 100644 index 01c0248f6..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsNativeEncodingDecodingTests.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams.integration; - -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Arrays; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.rule.EmbeddedKafkaRule; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -/** - * @author Soby Chacko - */ -@RunWith(SpringRunner.class) -@ContextConfiguration -@DirtiesContext -public abstract class KafkaStreamsNativeEncodingDecodingTests { - - /** - * Kafka rule. - */ - @ClassRule - public static EmbeddedKafkaRule embeddedKafkaRule = new EmbeddedKafkaRule(1, true, - "decode-counts", "decode-counts-1"); - - private static EmbeddedKafkaBroker embeddedKafka = embeddedKafkaRule - .getEmbeddedKafka(); - - @SpyBean - org.springframework.cloud.stream.binder.kafka.streams.KafkaStreamsMessageConversionDelegate conversionDelegate; - - private static Consumer consumer; - - @BeforeClass - public static void setUp() { - System.setProperty("spring.cloud.stream.kafka.streams.binder.brokers", - embeddedKafka.getBrokersAsString()); - System.setProperty("server.port", "0"); - System.setProperty("spring.jmx.enabled", "false"); - - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "decode-counts", "decode-counts-1"); - } - - @AfterClass - public static void tearDown() { - consumer.close(); - System.clearProperty("spring.cloud.stream.kafka.streams.binder.brokers"); - System.clearProperty("server.port"); - System.clearProperty("spring.jmx.enabled"); - } - - @SpringBootTest(classes = WordCountProcessorApplication.class, properties = { - "spring.cloud.stream.bindings.process-in-0.destination=decode-words-1", - "spring.cloud.stream.bindings.process-out-0.destination=decode-counts-1", - "spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.applicationId" - + "=NativeEncodingDecodingEnabledTests-abc" }, webEnvironment = SpringBootTest.WebEnvironment.NONE) - public static class NativeEncodingDecodingEnabledTests - extends KafkaStreamsNativeEncodingDecodingTests { - - @Test - public void test() throws Exception { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("decode-words-1"); - template.sendDefault("foobar"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - "decode-counts-1"); - assertThat(cr.value().equals("Count for foobar : 1")).isTrue(); - - verify(conversionDelegate, never()).serializeOnOutbound(any(KStream.class)); - verify(conversionDelegate, never()).deserializeOnInbound(any(Class.class), - any(KStream.class)); - } - - } - - @SpringBootTest(classes = WordCountProcessorApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE, - properties = { - "spring.cloud.stream.bindings.process-in-0.destination=decode-words", - "spring.cloud.stream.bindings.process-out-0.destination=decode-counts", - "spring.cloud.stream.bindings.process-in-0.consumer.useNativeDecoding=false", - "spring.cloud.stream.bindings.process-out-0.producer.useNativeEncoding=false", - "spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.applicationId" - + "=hello-NativeEncodingDecodingEnabledTests-xyz" }) - public static class NativeEncodingDecodingDisabledTests - extends KafkaStreamsNativeEncodingDecodingTests { - - @Test - public void test() { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("decode-words"); - Message msg = MessageBuilder.withPayload("foobar").setHeader("foo", "bar").build(); - template.send(msg); - - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - "decode-counts"); - - final Headers headers = cr.headers(); - final Iterable
foo = headers.headers("foo"); - assertThat(foo.iterator().hasNext()).isTrue(); - final Header fooHeader = foo.iterator().next(); - assertThat(fooHeader.value()).isEqualTo("bar".getBytes(StandardCharsets.UTF_8)); - - assertThat(cr.value().equals("Count for foobar : 1")).isTrue(); - - verify(conversionDelegate).serializeOnOutbound(any(KStream.class)); - verify(conversionDelegate).deserializeOnInbound(any(Class.class), - any(KStream.class)); - } - - } - - @EnableAutoConfiguration - public static class WordCountProcessorApplication { - - @Bean - public Function, KStream> process() { - - return input -> input - .flatMapValues( - value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(Serdes.String(), Serdes.String())) - .windowedBy(TimeWindows.of(Duration.ofSeconds(5))).count(Materialized.as("foo-WordCounts-x")) - .toStream().map((key, value) -> new KeyValue<>(null, - "Count for " + key.key() + " : " + value)); - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsStateStoreIntegrationTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsStateStoreIntegrationTests.java deleted file mode 100644 index efddbad18..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkaStreamsStateStoreIntegrationTests.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.streams.integration; - -import java.time.Duration; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.processor.Processor; -import org.apache.kafka.streams.processor.ProcessorContext; -import org.apache.kafka.streams.state.StoreBuilder; -import org.apache.kafka.streams.state.Stores; -import org.apache.kafka.streams.state.WindowStore; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static junit.framework.TestCase.fail; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Lei Chen - * @author Soby Chacko - */ -@EmbeddedKafka -public class KafkaStreamsStateStoreIntegrationTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void testKstreamStateStore() throws Exception { - SpringApplication app = new SpringApplication(ProductCountApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.bindings.input.destination=foobar", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId" - + "=KafkaStreamsStateStoreIntegrationTests-abc", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - Thread.sleep(2000); - receiveAndValidateFoo(context, ProductCountApplication.class); - } - catch (Exception e) { - throw e; - } - finally { - context.close(); - } - } - - @Test - void testSameStateStoreIsCreatedOnlyOnceWhenMultipleInputBindingsArePresent() throws Exception { - SpringApplication app = new SpringApplication(ProductCountApplicationWithMultipleInputBindings.class); - app.setWebApplicationType(WebApplicationType.NONE); - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input1", - "--spring.cloud.stream.function.bindings.process-in-1=input2", - "--spring.cloud.stream.bindings.input1.destination=foobar", - "--spring.cloud.stream.bindings.input2.destination=hello-foobar", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input1.consumer.applicationId" - + "=KafkaStreamsStateStoreIntegrationTests-abc", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - Thread.sleep(2000); - // We are not particularly interested in querying the state store here, as that is verified by the other test - // in this class. This test verifies that the same store is not attempted to be created by multiple input bindings. - // Normally, that will cause an exception to be thrown. However by not getting any exceptions, we are verifying - // that the binder is handling it appropriately. - //For more info, see this issue: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/551 - } - catch (Exception e) { - throw e; - } - finally { - context.close(); - } - } - - private void receiveAndValidateFoo(ConfigurableApplicationContext context, Class clazz) - throws Exception { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foobar"); - template.sendDefault("{\"id\":\"123\"}"); - Thread.sleep(1000); - - // assertions - if (clazz.isAssignableFrom(ProductCountApplication.class)) { - ProductCountApplication productCount = context - .getBean(ProductCountApplication.class); - WindowStore state = productCount.state; - assertThat(state != null).isTrue(); - assertThat(state.name()).isEqualTo("mystate"); - assertThat(state.persistent()).isTrue(); - assertThat(productCount.processed).isTrue(); - } - else { - fail("Expected assertions did not happen"); - } - - } - - @EnableAutoConfiguration - public static class ProductCountApplication { - - WindowStore state; - - boolean processed; - - @Bean - public Consumer> process() { - - return input -> input.process(() -> new Processor() { - - @Override - public void init(ProcessorContext processorContext) { - state = (WindowStore) processorContext.getStateStore("mystate"); - } - - @Override - public void process(Object s, Product product) { - processed = true; - } - - @Override - public void close() { - if (state != null) { - state.close(); - } - } - }, "mystate"); - } - - @Bean - public StoreBuilder mystore() { - return Stores.windowStoreBuilder( - Stores.persistentWindowStore("mystate", - Duration.ofMillis(3), Duration.ofMillis(3), false), Serdes.String(), - Serdes.String()); - } - } - - @EnableAutoConfiguration - public static class ProductCountApplicationWithMultipleInputBindings { - - WindowStore state; - - boolean processed; - - @Bean - public BiConsumer, KStream> process() { - - return (input, input2) -> { - - input.process(() -> new Processor() { - - @Override - public void init(ProcessorContext processorContext) { - state = (WindowStore) processorContext.getStateStore("mystate"); - } - - @Override - public void process(Object s, Product product) { - processed = true; - } - - @Override - public void close() { - if (state != null) { - state.close(); - } - } - }, "mystate"); - //simple use of input2, we are not using input2 for anything other than triggering some test behavior. - input2.foreach((key, value) -> { }); - }; - } - - @Bean - public StoreBuilder mystore() { - return Stores.windowStoreBuilder( - Stores.persistentWindowStore("mystate", - Duration.ofMillis(3), Duration.ofMillis(3), false), Serdes.String(), - Serdes.String()); - } - } - - public static class Product { - - Integer id; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkastreamsBinderPojoInputStringOutputIntegrationTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkastreamsBinderPojoInputStringOutputIntegrationTests.java deleted file mode 100644 index 9fc2f5a3d..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/integration/KafkastreamsBinderPojoInputStringOutputIntegrationTests.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.streams.integration; - -import java.time.Duration; -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.kstream.Grouped; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Materialized; -import org.apache.kafka.streams.kstream.TimeWindows; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.kafka.config.StreamsBuilderFactoryBean; -import org.springframework.kafka.core.CleanupConfig; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.serializer.JsonSerde; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Marius Bogoevici - * @author Soby Chacko - * @author Gary Russell - */ -@EmbeddedKafka(topics = "counts-id") -public class KafkastreamsBinderPojoInputStringOutputIntegrationTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static Consumer consumer; - - @BeforeAll - public static void setUp() throws Exception { - Map consumerProps = KafkaTestUtils.consumerProps("group-id", - "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( - consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, "counts-id"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testKstreamBinderWithPojoInputAndStringOuput() throws Exception { - SpringApplication app = new SpringApplication(ProductCountApplication.class); - app.setWebApplicationType(WebApplicationType.NONE); - ConfigurableApplicationContext context = app.run("--server.port=0", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.function.bindings.process-in-0=input", - "--spring.cloud.stream.function.bindings.process-out-0=output", - "--spring.cloud.stream.bindings.input.destination=foos", - "--spring.cloud.stream.bindings.output.destination=counts-id", - "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde" - + "=org.apache.kafka.common.serialization.Serdes$StringSerde", - "--spring.cloud.stream.kafka.streams.bindings.input.consumer.applicationId=ProductCountApplication-xyz", - "--spring.cloud.stream.kafka.streams.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - try { - receiveAndValidateFoo(); - // Assertions on StreamBuilderFactoryBean - StreamsBuilderFactoryBean streamsBuilderFactoryBean = context - .getBean("&stream-builder-process", StreamsBuilderFactoryBean.class); - CleanupConfig cleanup = TestUtils.getPropertyValue(streamsBuilderFactoryBean, - "cleanupConfig", CleanupConfig.class); - assertThat(cleanup.cleanupOnStart()).isFalse(); - assertThat(cleanup.cleanupOnStop()).isFalse(); - } - finally { - context.close(); - } - } - - private void receiveAndValidateFoo() { - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( - senderProps); - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("foos"); - template.sendDefault("{\"id\":\"123\"}"); - ConsumerRecord cr = KafkaTestUtils.getSingleRecord(consumer, - "counts-id"); - assertThat(cr.value().contains("Count for product with ID 123: 1")).isTrue(); - } - - @EnableAutoConfiguration - public static class ProductCountApplication { - - @Bean - public Function, KStream> process() { - return input -> input.filter((key, product) -> product.getId() == 123) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Grouped.with(new JsonSerde<>(Product.class), - new JsonSerde<>(Product.class))) - .windowedBy(TimeWindows.of(Duration.ofMillis(5000))) - .count(Materialized.as("id-count-store")).toStream() - .map((key, value) -> new KeyValue<>(key.key().id, - "Count for product with ID 123: " + value)); - } - - } - - static class Product { - - Integer id; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/serde/CollectionSerdeTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/serde/CollectionSerdeTest.java deleted file mode 100644 index c31715bcb..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/java/org/springframework/cloud/stream/binder/kafka/streams/serde/CollectionSerdeTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.streams.serde; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Soby Chacko - */ -public class CollectionSerdeTest { - - @Test - public void testCollectionsSerde() { - - Foo foo1 = new Foo(); - foo1.setData("data-1"); - foo1.setNum(1); - - Foo foo2 = new Foo(); - foo2.setData("data-2"); - foo2.setNum(2); - - List foos = new ArrayList<>(); - foos.add(foo1); - foos.add(foo2); - - CollectionSerde collectionSerde = new CollectionSerde<>(Foo.class, ArrayList.class); - byte[] serialized = collectionSerde.serializer().serialize("", foos); - - Collection deserialized = collectionSerde.deserializer().deserialize("", serialized); - - Iterator iterator = deserialized.iterator(); - Foo foo1Retrieved = iterator.next(); - assertThat(foo1Retrieved.getData()).isEqualTo("data-1"); - assertThat(foo1Retrieved.getNum()).isEqualTo(1); - - Foo foo2Retrieved = iterator.next(); - assertThat(foo2Retrieved.getData()).isEqualTo("data-2"); - assertThat(foo2Retrieved.getNum()).isEqualTo(2); - - } - - static class Foo { - - private int num; - private String data; - - Foo() { - } - - public int getNum() { - return num; - } - - public void setNum(int num) { - this.num = num; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/resources/logback.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/resources/logback.xml deleted file mode 100644 index 414797f87..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka-streams/src/test/resources/logback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - %d{ISO8601} %5p %t %c{2}:%L - %m%n - - - - - - - - - - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/.jdk8 b/binders/kafka-binder/spring-cloud-stream-binder-kafka/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/pom.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka/pom.xml deleted file mode 100644 index d986a6f98..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/pom.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-binder-kafka - jar - spring-cloud-stream-binder-kafka - Kafka binder implementation - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-parent - 4.1.0-SNAPSHOT - - - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-core - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.boot - spring-boot-autoconfigure - true - - - org.springframework.kafka - spring-kafka - - - org.springframework.boot - spring-boot-test - test - - - org.springframework.kafka - spring-kafka-test - test - - - org.springframework.cloud - spring-cloud-stream-test-support - test - - - org.awaitility - awaitility - test - - - io.micrometer - micrometer-observation-test - test - - - - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/BinderHeaderMapper.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/BinderHeaderMapper.java deleted file mode 100644 index 99dde2d1a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/BinderHeaderMapper.java +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.std.StdNodeBasedDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.TextNode; -import com.fasterxml.jackson.databind.type.TypeFactory; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.header.internals.RecordHeader; - -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.kafka.support.AbstractKafkaHeaderMapper; -import org.springframework.kafka.support.JacksonUtils; -import org.springframework.lang.Nullable; -import org.springframework.messaging.MessageHeaders; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.MimeType; - -/** - * Custom header mapper for Apache Kafka. This is identical to the {@link org.springframework.kafka.support.DefaultKafkaHeaderMapper} - * from spring Kafka. This is provided for addressing some interoperability issues between Spring Cloud Stream 3.0.x - * and 2.x apps, where mime types passed as regular {@link MimeType} in the header are not de-serialized properly. - * It also suppresses certain internal headers that should never be propagated on output. - * - * Most headers in {@link org.springframework.kafka.support.KafkaHeaders} are not mapped onto outbound messages. - * The exceptions are correlation and reply headers for request/reply - * messaging. - * Header types are added to a special header {@link #JSON_TYPES}. - * - * @author Gary Russell - * @author Artem Bilan - * @author Soby Chacko - * @author Byungjun You - * - * @since 3.0.0 - * - */ -public class BinderHeaderMapper extends AbstractKafkaHeaderMapper { - - private static final String NEGATE = "!"; - - private static final String NEVER_ID = NEGATE + MessageHeaders.ID; - - private static final String NEVER_TIMESTAMP = NEGATE + MessageHeaders.TIMESTAMP; - - private static final String NEVER_DELIVERY_ATTEMPT = NEGATE + IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT; - - private static final String NEVER_NATIVE_HEADERS_PRESENT = NEGATE + BinderHeaders.NATIVE_HEADERS_PRESENT; - - private static final String JAVA_LANG_STRING = "java.lang.String"; - - private static final List DEFAULT_TRUSTED_PACKAGES = - Arrays.asList( - "java.lang", - "java.net", - "java.util", - "org.springframework.util" - ); - - private static final List DEFAULT_TO_STRING_CLASSES = - Arrays.asList( - "org.springframework.util.MimeType", - "org.springframework.http.MediaType" - ); - - /** - * Header name for java types of other headers. - */ - public static final String JSON_TYPES = "spring_json_header_types"; - - private final ObjectMapper objectMapper; - - private final Set trustedPackages = new LinkedHashSet<>(DEFAULT_TRUSTED_PACKAGES); - - private final Set toStringClasses = new LinkedHashSet<>(DEFAULT_TO_STRING_CLASSES); - - private boolean encodeStrings; - - /** - * Construct an instance with the default object mapper and default header patterns - * for outbound headers; all inbound headers are mapped. The default pattern list is - * {@code "!id", "!timestamp" and "*"}. In addition, most of the headers in - * {@link org.springframework.kafka.support.KafkaHeaders} are never mapped as headers since they represent data in - * consumer/producer records. - * @see #BinderHeaderMapper(ObjectMapper) - */ - public BinderHeaderMapper() { - this(JacksonUtils.enhancedObjectMapper()); - } - - /** - * Construct an instance with the provided object mapper and default header patterns - * for outbound headers; all inbound headers are mapped. The patterns are applied in - * order, stopping on the first match (positive or negative). Patterns are negated by - * preceding them with "!". The default pattern list is - * {@code "!id", "!timestamp" and "*"}. In addition, most of the headers in - * {@link org.springframework.kafka.support.KafkaHeaders} are never mapped as headers since they represent data in - * consumer/producer records. - * @param objectMapper the object mapper. - * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) - */ - public BinderHeaderMapper(ObjectMapper objectMapper) { - this(objectMapper, - NEVER_ID, - NEVER_TIMESTAMP, - NEVER_DELIVERY_ATTEMPT, - NEVER_NATIVE_HEADERS_PRESENT, - "*"); - } - - /** - * Construct an instance with a default object mapper and the provided header patterns - * for outbound headers; all inbound headers are mapped. The patterns are applied in - * order, stopping on the first match (positive or negative). Patterns are negated by - * preceding them with "!". The patterns will replace the default patterns; you - * generally should not map the {@code "id" and "timestamp"} headers. Note: - * most of the headers in {@link org.springframework.kafka.support.KafkaHeaders} are ever mapped as headers since they - * represent data in consumer/producer records. - * @param patterns the patterns. - * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) - */ - public BinderHeaderMapper(String... patterns) { - this(new ObjectMapper(), patterns); - } - - /** - * Construct an instance with the provided object mapper and the provided header - * patterns for outbound headers; all inbound headers are mapped. The patterns are - * applied in order, stopping on the first match (positive or negative). Patterns are - * negated by preceding them with "!". The patterns will replace the default patterns; - * you generally should not map the {@code "id" and "timestamp"} headers. Note: most - * of the headers in {@link org.springframework.kafka.support.KafkaHeaders} are never mapped as headers since they - * represent data in consumer/producer records. - * @param objectMapper the object mapper. - * @param patterns the patterns. - * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) - */ - public BinderHeaderMapper(ObjectMapper objectMapper, String... patterns) { - super(patterns); - Assert.notNull(objectMapper, "'objectMapper' must not be null"); - Assert.noNullElements(patterns, "'patterns' must not have null elements"); - this.objectMapper = objectMapper; - this.objectMapper - .registerModule(new SimpleModule().addDeserializer(MimeType.class, new MimeTypeJsonDeserializer())); - } - - /** - * Return the object mapper. - * @return the mapper. - */ - protected ObjectMapper getObjectMapper() { - return this.objectMapper; - } - - /** - * Provide direct access to the trusted packages set for subclasses. - * @return the trusted packages. - * @since 2.2 - */ - protected Set getTrustedPackages() { - return this.trustedPackages; - } - - /** - * Provide direct access to the toString() classes by subclasses. - * @return the toString() classes. - * @since 2.2 - */ - protected Set getToStringClasses() { - return this.toStringClasses; - } - - protected boolean isEncodeStrings() { - return this.encodeStrings; - } - - /** - * Set to true to encode String-valued headers as JSON ("..."), by default just the - * raw String value is converted to a byte array using the configured charset. Set to - * true if a consumer of the outbound record is using Spring for Apache Kafka version - * less than 2.3 - * @param encodeStrings true to encode (default false). - * @since 2.3 - */ - public void setEncodeStrings(boolean encodeStrings) { - this.encodeStrings = encodeStrings; - } - - /** - * Add packages to the trusted packages list (default {@code java.util, java.lang}) used - * when constructing objects from JSON. - * If any of the supplied packages is {@code "*"}, all packages are trusted. - * If a class for a non-trusted package is encountered, the header is returned to the - * application with value of type {@link NonTrustedHeaderType}. - * @param packagesToTrust the packages to trust. - */ - public void addTrustedPackages(String... packagesToTrust) { - if (packagesToTrust != null) { - for (String whiteList : packagesToTrust) { - if ("*".equals(whiteList)) { - this.trustedPackages.clear(); - break; - } - else { - this.trustedPackages.add(whiteList); - } - } - } - } - - /** - * Add class names that the outbound mapper should perform toString() operations on - * before mapping. - * @param classNames the class names. - * @since 2.2 - */ - public void addToStringClasses(String... classNames) { - this.toStringClasses.addAll(Arrays.asList(classNames)); - } - - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - final Map jsonHeaders = new HashMap<>(); - final ObjectMapper headerObjectMapper = getObjectMapper(); - headers.forEach((key, rawValue) -> { - if (matches(key, rawValue)) { - Object valueToAdd = headerValueToAddOut(key, rawValue); - if (valueToAdd instanceof byte[] valueToAddBytes) { - target.add(new RecordHeader(key, valueToAddBytes)); - } - else { - try { - String className = valueToAdd.getClass().getName(); - if (this.toStringClasses.contains(className)) { - valueToAdd = valueToAdd.toString(); - className = JAVA_LANG_STRING; - } - if (!this.encodeStrings - && !MimeType.class.isAssignableFrom(rawValue.getClass()) - && valueToAdd instanceof String stringValueToAdd) { - target.add(new RecordHeader(key, stringValueToAdd.getBytes(getCharset()))); - className = JAVA_LANG_STRING; - } - else { - target.add(new RecordHeader(key, headerObjectMapper.writeValueAsBytes(valueToAdd))); - } - jsonHeaders.put(key, className); - } - catch (Exception e) { - logger.debug(e, () -> "Could not map " + key + " with type " + rawValue.getClass().getName()); - } - } - } - }); - if (jsonHeaders.size() > 0) { - try { - target.add(new RecordHeader(JSON_TYPES, headerObjectMapper.writeValueAsBytes(jsonHeaders))); - } - catch (IllegalStateException | JsonProcessingException e) { - logger.error(e, "Could not add json types header"); - } - } - } - - @Override - public void toHeaders(Headers source, final Map headers) { - final Map jsonTypes = decodeJsonTypes(source); - source.forEach(header -> { - if (!(header.key().equals(JSON_TYPES))) { - if (jsonTypes != null && jsonTypes.containsKey(header.key())) { - String requestedType = jsonTypes.get(header.key()); - populateJsonValueHeader(header, requestedType, headers); - } - else { - headers.put(header.key(), headerValueToAddIn(header)); - } - } - }); - } - - private void populateJsonValueHeader(Header header, String requestedType, Map headers) { - Class type = Object.class; - boolean trusted = false; - try { - trusted = trusted(requestedType); - if (trusted) { - type = ClassUtils.forName(requestedType, null); - } - } - catch (Exception e) { - logger.error(e, () -> "Could not load class for header: " + header.key()); - } - if (String.class.equals(type) && (header.value().length == 0 || header.value()[0] != '"')) { - headers.put(header.key(), new String(header.value(), getCharset())); - } - else { - if (trusted) { - try { - Object value = decodeValue(header, type); - headers.put(header.key(), value); - } - catch (IOException e) { - logger.error(e, () -> - "Could not decode json type: " + new String(header.value()) + " for key: " - + header.key()); - headers.put(header.key(), header.value()); - } - } - else { - headers.put(header.key(), new NonTrustedHeaderType(header.value(), requestedType)); - } - } - } - - private Object decodeValue(Header h, Class type) throws IOException, LinkageError { - ObjectMapper headerObjectMapper = getObjectMapper(); - Object value = headerObjectMapper.readValue(h.value(), type); - if (type.equals(NonTrustedHeaderType.class)) { - // Upstream NTHT propagated; may be trusted here... - NonTrustedHeaderType nth = (NonTrustedHeaderType) value; - if (trusted(nth.getUntrustedType())) { - try { - value = headerObjectMapper.readValue(nth.getHeaderValue(), - ClassUtils.forName(nth.getUntrustedType(), null)); - } - catch (Exception e) { - logger.error(e, () -> "Could not decode header: " + nth); - } - } - } - return value; - } - - @SuppressWarnings("unchecked") - @Nullable - private Map decodeJsonTypes(Headers source) { - Map types = null; - Header jsonTypes = source.lastHeader(JSON_TYPES); - if (jsonTypes != null) { - ObjectMapper headerObjectMapper = getObjectMapper(); - try { - types = headerObjectMapper.readValue(jsonTypes.value(), Map.class); - } - catch (IOException e) { - logger.error(e, () -> "Could not decode json types: " + new String(jsonTypes.value())); - } - } - return types; - } - - protected boolean trusted(String requestedType) { - if (requestedType.equals(NonTrustedHeaderType.class.getName())) { - return true; - } - if (!this.trustedPackages.isEmpty()) { - int lastDot = requestedType.lastIndexOf('.'); - if (lastDot < 0) { - return false; - } - String packageName = requestedType.substring(0, lastDot); - for (String trustedPackage : this.trustedPackages) { - if (packageName.equals(trustedPackage) || packageName.startsWith(trustedPackage + ".")) { - return true; - } - } - return false; - } - return true; - } - - /** - * Add patterns for headers that should never be mapped. - * @param patterns the patterns. - * @return the modified patterns. - * @since 3.0.2 - */ - public static String[] addNeverHeaderPatterns(List patterns) { - List patternsToUse = new LinkedList<>(patterns); - patternsToUse.add(0, NEVER_NATIVE_HEADERS_PRESENT); - patternsToUse.add(0, NEVER_DELIVERY_ATTEMPT); - patternsToUse.add(0, NEVER_TIMESTAMP); - patternsToUse.add(0, NEVER_ID); - return patternsToUse.toArray(new String[0]); - } - - /** - * Remove never headers. - * @param headers the headers from which to remove the never headers. - * @since 3.0.2 - */ - public static void removeNeverHeaders(Headers headers) { - headers.remove(MessageHeaders.ID); - headers.remove(MessageHeaders.TIMESTAMP); - headers.remove(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); - headers.remove(BinderHeaders.NATIVE_HEADERS_PRESENT); - } - - /** - * The {@link StdNodeBasedDeserializer} extension for {@link MimeType} deserialization. - * It is presented here for backward compatibility when older producers send {@link MimeType} - * headers as serialization version. - */ - private class MimeTypeJsonDeserializer extends StdNodeBasedDeserializer { - - private static final long serialVersionUID = 1L; - - MimeTypeJsonDeserializer() { - super(MimeType.class); - } - - @Override - public MimeType convert(JsonNode root, DeserializationContext ctxt) throws IOException { - if (root instanceof TextNode) { - return MimeType.valueOf(root.asText()); - } - else { - JsonNode type = root.get("type"); - JsonNode subType = root.get("subtype"); - JsonNode parameters = root.get("parameters"); - Map params = - BinderHeaderMapper.this.objectMapper.readValue(parameters.traverse(), - TypeFactory.defaultInstance() - .constructMapType(HashMap.class, String.class, String.class)); - return new MimeType(type.asText(), subType.asText(), params); - } - } - - } - - /** - * Represents a header that could not be decoded due to an untrusted type. - */ - public static class NonTrustedHeaderType { - - private byte[] headerValue; - - private String untrustedType; - - public NonTrustedHeaderType() { - super(); - } - - NonTrustedHeaderType(byte[] headerValue, String untrustedType) { // NOSONAR - this.headerValue = headerValue; // NOSONAR - this.untrustedType = untrustedType; - } - - - public void setHeaderValue(byte[] headerValue) { // NOSONAR - this.headerValue = headerValue; // NOSONAR array reference - } - - public byte[] getHeaderValue() { - return this.headerValue; // NOSONAR - } - - public void setUntrustedType(String untrustedType) { - this.untrustedType = untrustedType; - } - - public String getUntrustedType() { - return this.untrustedType; - } - - @Override - public String toString() { - try { - return "NonTrustedHeaderType [headerValue=" + new String(this.headerValue, StandardCharsets.UTF_8) - + ", untrustedType=" + this.untrustedType + "]"; - } - catch (@SuppressWarnings("unused") Exception e) { - return "NonTrustedHeaderType [headerValue=" + Arrays.toString(this.headerValue) + ", untrustedType=" - + this.untrustedType + "]"; - } - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealth.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealth.java deleted file mode 100644 index dd6e6b6b1..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealth.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka; - -import org.springframework.boot.actuate.health.HealthIndicator; - -/** - * Marker interface used for custom KafkaBinderHealth indicator implementations. - * - * @author Soby Chacko - * @since 3.2.2 - */ -public interface KafkaBinderHealth extends HealthIndicator { - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealthIndicator.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealthIndicator.java deleted file mode 100644 index dc412dd2a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealthIndicator.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -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.Executors; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.cloud.stream.binder.kafka.common.AbstractKafkaBinderHealthIndicator; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.scheduling.concurrent.CustomizableThreadFactory; - -/** - * Health indicator for Kafka. - * - * @author Ilayaperumal Gopinathan - * @author Marius Bogoevici - * @author Henryk Konsek - * @author Gary Russell - * @author Laur Aliste - * @author Soby Chacko - * @author Vladislav Fefelov - * @author Chukwubuikem Ume-Ugwa - * @author Taras Danylchuk - */ -public class KafkaBinderHealthIndicator extends AbstractKafkaBinderHealthIndicator { - - private final KafkaMessageChannelBinder binder; - - - public KafkaBinderHealthIndicator(KafkaMessageChannelBinder binder, - ConsumerFactory consumerFactory) { - super(consumerFactory); - this.binder = binder; - } - - @Override - protected ExecutorService createHealthBinderExecutorService() { - return Executors.newSingleThreadExecutor( - new CustomizableThreadFactory("kafka-binder-health-")); - } - - @Override - protected Map getTopicsInUse() { - return this.binder.getTopicsInUse(); - } - - @Override - protected Health buildBinderSpecificHealthDetails() { - List> listenerContainers = binder.getKafkaMessageListenerContainers(); - if (listenerContainers.isEmpty()) { - return Health.unknown().build(); - } - - Status status = Status.UP; - List> containersDetails = new ArrayList<>(); - - for (AbstractMessageListenerContainer container : listenerContainers) { - Map containerDetails = new HashMap<>(); - boolean isRunning = container.isRunning(); - boolean isOk = container.isInExpectedState(); - if (!isOk) { - status = Status.DOWN; - } - containerDetails.put("isRunning", isRunning); - containerDetails.put("isStoppedAbnormally", !isRunning && !isOk); - containerDetails.put("isPaused", container.isContainerPaused()); - containerDetails.put("listenerId", container.getListenerId()); - containerDetails.put("groupId", container.getGroupId()); - - containersDetails.add(containerDetails); - } - return Health.status(status) - .withDetail("listenerContainers", containersDetails) - .build(); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderMetrics.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderMetrics.java deleted file mode 100644 index c5bba0eb5..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderMetrics.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.ToDoubleFunction; - -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.MeterBinder; -import io.micrometer.core.instrument.noop.NoopGauge; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.OffsetAndMetadata; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.serialization.ByteArrayDeserializer; - -import org.springframework.cloud.stream.binder.BindingCreatedEvent; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.context.ApplicationListener; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; - -/** - * Metrics for Kafka binder. - * - * @author Henryk Konsek - * @author Soby Chacko - * @author Artem Bilan - * @author Oleg Zhurakousky - * @author Jon Schneider - * @author Thomas Cheyney - * @author Gary Russell - * @author Lars Bilger - * @author Tomek Szmytka - * @author Nico Heller - */ -public class KafkaBinderMetrics - implements MeterBinder, ApplicationListener, AutoCloseable { - - private static final int DEFAULT_TIMEOUT = 5; - - private static final Log LOG = LogFactory.getLog(KafkaBinderMetrics.class); - - /** - * Offset lag micrometer metric name. This can be used for meter filtering. - */ - public static final String OFFSET_LAG_METRIC_NAME = "spring.cloud.stream.binder.kafka.offset"; - - private final KafkaMessageChannelBinder binder; - - private final KafkaBinderConfigurationProperties binderConfigurationProperties; - - private ConsumerFactory defaultConsumerFactory; - - private final MeterRegistry meterRegistry; - - private Map> metadataConsumers; - - private int timeout = DEFAULT_TIMEOUT; - - private final Map lastUnconsumedMessagesValues = new ConcurrentHashMap<>(); - - ScheduledExecutorService scheduler; - - public KafkaBinderMetrics(KafkaMessageChannelBinder binder, - KafkaBinderConfigurationProperties binderConfigurationProperties, - ConsumerFactory defaultConsumerFactory, - @Nullable MeterRegistry meterRegistry) { - - this.binder = binder; - this.binderConfigurationProperties = binderConfigurationProperties; - this.defaultConsumerFactory = defaultConsumerFactory; - this.meterRegistry = meterRegistry; - this.metadataConsumers = new ConcurrentHashMap<>(); - } - - public KafkaBinderMetrics(KafkaMessageChannelBinder binder, - KafkaBinderConfigurationProperties binderConfigurationProperties) { - - this(binder, binderConfigurationProperties, null, null); - } - - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - @Override - public void bindTo(MeterRegistry registry) { - /** - * We can't just replace one scheduler with another. - * Before and even after the old one is gathered by GC, it's threads still exist, consume memory and CPU resources to switch contexts. - * Theoretically, as a result of processing n topics, there will be about (1+n)*n/2 threads simultaneously at the same time. - */ - if (this.scheduler != null) { - LOG.info("Try to shutdown the old scheduler with " + ((ScheduledThreadPoolExecutor) scheduler).getPoolSize() + " threads"); - this.scheduler.shutdown(); - } - - this.scheduler = Executors.newScheduledThreadPool(this.binder.getTopicsInUse().size()); - - for (Map.Entry topicInfo : this.binder - .getTopicsInUse().entrySet()) { - - if (!topicInfo.getValue().isConsumerTopic()) { - continue; - } - - String topic = topicInfo.getKey(); - String group = topicInfo.getValue().consumerGroup(); - - ToDoubleFunction offsetComputation = computeOffsetComputationFunction(topic, group); - final Gauge register = Gauge.builder(OFFSET_LAG_METRIC_NAME, this, offsetComputation) - .tag("group", group) - .tag("topic", topic) - .description("Unconsumed messages for a particular group and topic") - .register(registry); - - if (!(register instanceof NoopGauge)) { - lastUnconsumedMessagesValues.put(topic + "-" + group, 0L); - this.scheduler.scheduleWithFixedDelay( - () -> computeUnconsumedMessages(topic, group), - 1, - binderConfigurationProperties.getMetrics().getOffsetLagMetricsInterval().toSeconds(), - TimeUnit.SECONDS - ); - } - } - } - - private ToDoubleFunction computeOffsetComputationFunction(String topic, String group) { - if (this.binderConfigurationProperties.getMetrics().isDefaultOffsetLagMetricsEnabled()) { - return (o) -> computeAndGetUnconsumedMessagesWithTimeout(topic, group); - } - else { - return (o) -> lastUnconsumedMessagesValues.get(topic + "-" + group); - } - } - - private long computeAndGetUnconsumedMessagesWithTimeout(String topic, String group) { - Future future = scheduler.submit(() -> computeUnconsumedMessages(topic, group)); - try { - return future.get(this.timeout, TimeUnit.SECONDS); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - return lastUnconsumedMessagesValues.get(topic + "-" + group); - } - catch (ExecutionException | TimeoutException ex) { - return lastUnconsumedMessagesValues.get(topic + "-" + group); - } - } - - private long computeUnconsumedMessages(String topic, String group) { - long lag = 0; - try { - lag = findTotalTopicGroupLag(topic, group, this.metadataConsumers); - this.lastUnconsumedMessagesValues.put(topic + "-" + group, lag); - } - catch (Exception ex) { - LOG.debug("Cannot generate metric for topic: " + topic, ex); - } - return lag; - } - - private long findTotalTopicGroupLag(String topic, String group, Map> metadataConsumers) { - long lag = 0; - Consumer metadataConsumer = metadataConsumers.computeIfAbsent( - group, - (g) -> createConsumerFactory().createConsumer(g, "monitoring")); - List partitionInfos = metadataConsumer - .partitionsFor(topic); - List topicPartitions = new LinkedList<>(); - for (PartitionInfo partitionInfo : partitionInfos) { - topicPartitions.add(new TopicPartition(partitionInfo.topic(), - partitionInfo.partition())); - } - - Map endOffsets = metadataConsumer - .endOffsets(topicPartitions); - - final Map committedOffsets = metadataConsumer.committed(endOffsets.keySet()); - final Map beginningOffsets = metadataConsumer.beginningOffsets(endOffsets.keySet()); - - for (Map.Entry endOffset : endOffsets - .entrySet()) { - OffsetAndMetadata current = committedOffsets.get(endOffset.getKey()); - Long beginningOffset = beginningOffsets.get(endOffset.getKey()); - lag += endOffset.getValue(); - if (current != null) { - lag -= current.offset(); - } - else if (beginningOffset != null) { - lag -= beginningOffset; - } - } - return lag; - } - - private synchronized ConsumerFactory createConsumerFactory() { - if (this.defaultConsumerFactory == null) { - Map props = new HashMap<>(); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - Map mergedConfig = this.binderConfigurationProperties - .mergedConsumerConfiguration(); - if (!ObjectUtils.isEmpty(mergedConfig)) { - props.putAll(mergedConfig); - } - if (!props.containsKey(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG)) { - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, - this.binderConfigurationProperties - .getKafkaConnectionString()); - } - this.defaultConsumerFactory = new DefaultKafkaConsumerFactory<>( - props); - } - return this.defaultConsumerFactory; - } - - @Override - public void onApplicationEvent(BindingCreatedEvent event) { - if (this.meterRegistry != null) { - // It is safe to call bindTo multiple times, since meters are idempotent when called with the same arguments - this.bindTo(this.meterRegistry); - } - } - - @Override - public void close() throws Exception { - Optional.ofNullable(scheduler).ifPresent(ExecutorService::shutdown); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBindingRebalanceListener.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBindingRebalanceListener.java deleted file mode 100644 index 4bcc62a49..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaBindingRebalanceListener.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka; - -import java.util.Collection; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.common.TopicPartition; - -/** - * A rebalance listener that provides access to the binding name consumer object. It can - * be used to perform seek operations on the consumer after a rebalance. - * - * @author Gary Russell - * @since 2.1 - * - */ -public interface KafkaBindingRebalanceListener { - - /** - * Invoked by the container before any pending offsets are committed. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - */ - default void onPartitionsRevokedBeforeCommit(String bindingName, - Consumer consumer, Collection partitions) { - // do nothing - } - - /** - * Invoked by the container after any pending offsets are committed. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - */ - default void onPartitionsRevokedAfterCommit(String bindingName, - Consumer consumer, Collection partitions) { - // do nothing - } - - /** - * Invoked when partitions are initially assigned or after a rebalance. Applications - * might only want to perform seek operations on an initial assignment. While the - * 'initial' argument is true for each thread (when concurrency is greater than 1), - * implementations should keep track of exactly which partitions have been sought. - * There is a race in that a rebalance could occur during startup and so a topic/ - * partition that has been sought on one thread may be re-assigned to another - * thread and you may not wish to re-seek it at that time. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - * @param initial true if this is the initial assignment on the current thread. - */ - default void onPartitionsAssigned(String bindingName, Consumer consumer, - Collection partitions, boolean initial) { - // do nothing - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaExpressionEvaluatingInterceptor.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaExpressionEvaluatingInterceptor.java deleted file mode 100644 index 070329092..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaExpressionEvaluatingInterceptor.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka; - -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.util.Assert; - -/** - * Interceptor to evaluate expressions for outbound messages before serialization. - * - * @author Gary Russell - * @since 3.0 - * - */ -public class KafkaExpressionEvaluatingInterceptor implements ChannelInterceptor { - - /** - * Name for the evaluated message key header. - */ - public static final String MESSAGE_KEY_HEADER = "scst_messageKey"; - - private final Expression messageKeyExpression; - - private final EvaluationContext evaluationContext; - - /** - * Construct an instance with the provided expressions and evaluation context. At - * least one expression muse be non-null. - * @param messageKeyExpression the routing key expression. - * @param evaluationContext the evaluation context. - */ - public KafkaExpressionEvaluatingInterceptor(Expression messageKeyExpression, EvaluationContext evaluationContext) { - Assert.notNull(messageKeyExpression != null, "A message key expression is required"); - Assert.notNull(evaluationContext, "the 'evaluationContext' cannot be null"); - this.messageKeyExpression = messageKeyExpression; - this.evaluationContext = evaluationContext; - } - - @Override - public Message preSend(Message message, MessageChannel channel) { - MessageBuilder builder = MessageBuilder.fromMessage(message); - if (this.messageKeyExpression != null) { - builder.setHeader(MESSAGE_KEY_HEADER, - this.messageKeyExpression.getValue(this.evaluationContext, message)); - } - return builder.build(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java deleted file mode 100644 index 77d12d37f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java +++ /dev/null @@ -1,1674 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.Producer; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.header.internals.RecordHeaders; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.EmbeddedHeaderUtils; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.HeaderMode; -import org.springframework.cloud.stream.binder.MessageValues; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.cloud.stream.binder.kafka.config.ClientFactoryCustomizer; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.support.ConsumerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.utils.BindingUtils; -import org.springframework.cloud.stream.binder.kafka.utils.DlqDestinationResolver; -import org.springframework.cloud.stream.binder.kafka.utils.DlqPartitionFunction; -import org.springframework.cloud.stream.binding.DefaultPartitioningInterceptor; -import org.springframework.cloud.stream.binding.MessageConverterConfigurer.PartitioningInterceptor; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.common.LiteralExpression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.StaticMessageHeaderAccessor; -import org.springframework.integration.acks.AcknowledgmentCallback; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.expression.ExpressionUtils; -import org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter; -import org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter.ListenerMode; -import org.springframework.integration.kafka.inbound.KafkaMessageSource; -import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; -import org.springframework.integration.kafka.support.RawRecordHeaderErrorMessageStrategy; -import org.springframework.integration.support.ErrorMessageStrategy; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaAdmin; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.kafka.listener.CommonErrorHandler; -import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; -import org.springframework.kafka.listener.ConsumerAwareRebalanceListener; -import org.springframework.kafka.listener.ConsumerProperties; -import org.springframework.kafka.listener.ContainerProperties; -import org.springframework.kafka.listener.DefaultAfterRollbackProcessor; -import org.springframework.kafka.listener.DefaultErrorHandler; -import org.springframework.kafka.support.Acknowledgment; -import org.springframework.kafka.support.ExponentialBackOffWithMaxRetries; -import org.springframework.kafka.support.KafkaHeaderMapper; -import org.springframework.kafka.support.KafkaHeaders; -import org.springframework.kafka.support.ProducerListener; -import org.springframework.kafka.support.SendResult; -import org.springframework.kafka.support.TopicPartitionOffset; -import org.springframework.kafka.support.TopicPartitionOffset.SeekPosition; -import org.springframework.kafka.support.converter.MessageConverter; -import org.springframework.kafka.support.converter.MessagingMessageConverter; -import org.springframework.kafka.support.converter.RecordMessageConverter; -import org.springframework.kafka.transaction.KafkaAwareTransactionManager; -import org.springframework.kafka.transaction.KafkaTransactionManager; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.messaging.support.InterceptableChannel; -import org.springframework.transaction.support.TransactionTemplate; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; -import org.springframework.util.backoff.BackOff; -import org.springframework.util.backoff.ExponentialBackOff; -import org.springframework.util.backoff.FixedBackOff; - -/** - * A {@link org.springframework.cloud.stream.binder.Binder} that uses Kafka as the - * underlying middleware. - * - * @author Eric Bottard - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author David Turanski - * @author Gary Russell - * @author Mark Fisher - * @author Soby Chacko - * @author Henryk Konsek - * @author Doug Saus - * @author Lukasz Kaminski - * @author Taras Danylchuk - * @author Yi Liu - * @author Chris Bono - * @author Byungjun You - * @author Oliver FΓΌhrer - */ -public class KafkaMessageChannelBinder extends - // @checkstyle:off - AbstractMessageChannelBinder, ExtendedProducerProperties, KafkaTopicProvisioner> - // @checkstyle:on - implements - ExtendedPropertiesBinder { - - /** - * Kafka header for x-exception-fqcn. - */ - public static final String X_EXCEPTION_FQCN = "x-exception-fqcn"; - - /** - * Kafka header for x-exception-stacktrace. - */ - public static final String X_EXCEPTION_STACKTRACE = "x-exception-stacktrace"; - - /** - * Kafka header for x-exception-message. - */ - public static final String X_EXCEPTION_MESSAGE = "x-exception-message"; - - /** - * Kafka header for x-original-topic. - */ - public static final String X_ORIGINAL_TOPIC = "x-original-topic"; - - /** - * Kafka header for x-original-partition. - */ - public static final String X_ORIGINAL_PARTITION = "x-original-partition"; - - /** - * Kafka header for x-original-offset. - */ - public static final String X_ORIGINAL_OFFSET = "x-original-offset"; - - /** - * Kafka header for x-original-timestamp. - */ - public static final String X_ORIGINAL_TIMESTAMP = "x-original-timestamp"; - - /** - * Kafka header for x-original-timestamp-type. - */ - public static final String X_ORIGINAL_TIMESTAMP_TYPE = "x-original-timestamp-type"; - - private static final Pattern interceptorNeededPattern = Pattern.compile("(payload|#root|#this)"); - - private static final SpelExpressionParser PARSER = new SpelExpressionParser(); - - private final KafkaBinderConfigurationProperties configurationProperties; - - private final Map topicsInUse = new ConcurrentHashMap<>(); - - private final KafkaTransactionManager transactionManager; - - private final TransactionTemplate transactionTemplate; - - private KafkaBindingRebalanceListener rebalanceListener; - - private DlqPartitionFunction dlqPartitionFunction; - - private DlqDestinationResolver dlqDestinationResolver; - - private final Map ackModeInfo = new ConcurrentHashMap<>(); - - private ProducerListener producerListener; - - private KafkaExtendedBindingProperties extendedBindingProperties = new KafkaExtendedBindingProperties(); - - private final List clientFactoryCustomizers = new ArrayList<>(); - - private final List producerConfigCustomizers = new ArrayList<>(); - - private final List consumerConfigCustomizers = new ArrayList<>(); - - private final List> kafkaMessageListenerContainers = new ArrayList<>(); - - private final KafkaAdmin kafkaAdmin; - - public KafkaMessageChannelBinder( - KafkaBinderConfigurationProperties configurationProperties, - KafkaTopicProvisioner provisioningProvider) { - - this(configurationProperties, provisioningProvider, null, null, null, - null, null); - } - - public KafkaMessageChannelBinder( - KafkaBinderConfigurationProperties configurationProperties, - KafkaTopicProvisioner provisioningProvider, - ListenerContainerCustomizer> containerCustomizer, - KafkaBindingRebalanceListener rebalanceListener) { - - this(configurationProperties, provisioningProvider, containerCustomizer, null, rebalanceListener, - null, null); - } - - public KafkaMessageChannelBinder( - KafkaBinderConfigurationProperties configurationProperties, - KafkaTopicProvisioner provisioningProvider, - ListenerContainerCustomizer> containerCustomizer, - MessageSourceCustomizer> sourceCustomizer, - KafkaBindingRebalanceListener rebalanceListener, - DlqPartitionFunction dlqPartitionFunction, - DlqDestinationResolver dlqDestinationResolver) { - - super(headersToMap(configurationProperties), provisioningProvider, - containerCustomizer, sourceCustomizer); - this.configurationProperties = configurationProperties; - String txId = configurationProperties.getTransaction().getTransactionIdPrefix(); - if (StringUtils.hasText(txId)) { - this.transactionManager = new KafkaTransactionManager<>(getProducerFactory( - txId, new ExtendedProducerProperties<>(configurationProperties - .getTransaction().getProducer().getExtension()), txId + ".producer", null)); - this.transactionTemplate = new TransactionTemplate(this.transactionManager); - } - else { - this.transactionManager = null; - this.transactionTemplate = null; - } - this.rebalanceListener = rebalanceListener; - this.dlqPartitionFunction = dlqPartitionFunction; - this.dlqDestinationResolver = dlqDestinationResolver; - this.kafkaAdmin = new KafkaAdmin(new HashMap<>(provisioningProvider.getAdminClientProperties())); - } - - private static String[] headersToMap( - KafkaBinderConfigurationProperties configurationProperties) { - String[] headersToMap; - if (ObjectUtils.isEmpty(configurationProperties.getHeaders())) { - headersToMap = BinderHeaders.STANDARD_HEADERS; - } - else { - String[] combinedHeadersToMap = Arrays.copyOfRange( - BinderHeaders.STANDARD_HEADERS, 0, - BinderHeaders.STANDARD_HEADERS.length - + configurationProperties.getHeaders().length); - System.arraycopy(configurationProperties.getHeaders(), 0, - combinedHeadersToMap, BinderHeaders.STANDARD_HEADERS.length, - configurationProperties.getHeaders().length); - headersToMap = combinedHeadersToMap; - } - return headersToMap; - } - - public void setExtendedBindingProperties( - KafkaExtendedBindingProperties extendedBindingProperties) { - this.extendedBindingProperties = extendedBindingProperties; - } - - public void setProducerListener(ProducerListener producerListener) { - this.producerListener = producerListener; - } - - /** - * Set a {@link ClientFactoryCustomizer} for the {@link ProducerFactory} and {@link ConsumerFactory} created inside - * the binder. - * - * @param customizer the client factory customizer - * @deprecated in favor of {@link #addClientFactoryCustomizer(ClientFactoryCustomizer)}. - */ - @Deprecated - public void setClientFactoryCustomizer(ClientFactoryCustomizer customizer) { - addClientFactoryCustomizer(customizer); - } - - public void addClientFactoryCustomizer(ClientFactoryCustomizer customizer) { - if (customizer != null) { - this.clientFactoryCustomizers.add(customizer); - } - } - - public void setRebalanceListener(KafkaBindingRebalanceListener rebalanceListener) { - this.rebalanceListener = rebalanceListener; - } - - public void setDlqPartitionFunction(DlqPartitionFunction dlqPartitionFunction) { - this.dlqPartitionFunction = dlqPartitionFunction; - } - - public void setDlqDestinationResolver(DlqDestinationResolver dlqDestinationResolver) { - this.dlqDestinationResolver = dlqDestinationResolver; - } - - Map getTopicsInUse() { - return this.topicsInUse; - } - - @Override - public KafkaConsumerProperties getExtendedConsumerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedConsumerProperties(channelName); - } - - @Override - public KafkaProducerProperties getExtendedProducerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedProducerProperties(channelName); - } - - @Override - public String getDefaultsPrefix() { - return this.extendedBindingProperties.getDefaultsPrefix(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return this.extendedBindingProperties.getExtendedPropertiesEntryClass(); - } - - /** - * Return a reference to the binder's transaction manager's producer factory (if - * configured). Use this to create a transaction manager in a bean definition when you - * wish to use producer-only transactions. - * @return the transaction manager, or null. - */ - @Nullable - public ProducerFactory getTransactionalProducerFactory() { - return this.transactionManager == null ? null : this.transactionManager.getProducerFactory(); - } - - @Override - public String getBinderIdentity() { - return "kafka-" + super.getBinderIdentity(); - } - - @Override - protected MessageHandler createProducerMessageHandler( - final ProducerDestination destination, - ExtendedProducerProperties producerProperties, - MessageChannel errorChannel) throws Exception { - throw new IllegalStateException( - "The abstract binder should not call this method"); - } - - @Override - protected MessageHandler createProducerMessageHandler( - final ProducerDestination destination, - ExtendedProducerProperties producerProperties, - MessageChannel channel, MessageChannel errorChannel) throws Exception { - /* - * IMPORTANT: With a transactional binder, individual producer properties for - * Kafka are ignored; the global binder - * (spring.cloud.stream.kafka.binder.transaction.producer.*) properties are used - * instead, for all producers. A binder is transactional when - * 'spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix' has text. - * Individual bindings can override the binder's transaction manager. - */ - KafkaAwareTransactionManager transMan = transactionManager( - producerProperties.getExtension().getTransactionManager()); - final ProducerFactory producerFB = transMan != null - ? transMan.getProducerFactory() - : getProducerFactory(null, producerProperties, destination.getName() + ".producer", - destination.getName()); - Collection partitions = provisioningProvider.getPartitionsForTopic( - producerProperties.getPartitionCount(), false, () -> { - Producer producer = producerFB.createProducer(); - List partitionsFor = producer - .partitionsFor(destination.getName()); - producer.close(); - return partitionsFor; - }, destination.getName()); - this.topicsInUse.put(destination.getName(), - new TopicInformation(null, partitions, false)); - if (producerProperties.isPartitioned() - && producerProperties.getPartitionCount() < partitions.size()) { - if (this.logger.isInfoEnabled()) { - this.logger.info("The `partitionCount` of the producer for topic " - + destination.getName() + " is " - + producerProperties.getPartitionCount() - + ", smaller than the actual partition count of " - + partitions.size() - + " for the topic. The larger number will be used instead."); - } - List interceptors = ((InterceptableChannel) channel) - .getInterceptors(); - interceptors.forEach((interceptor) -> { - if (interceptor instanceof PartitioningInterceptor partitioningInterceptor) { - partitioningInterceptor.setPartitionCount(partitions.size()); - } - else if (interceptor instanceof DefaultPartitioningInterceptor defaultPartitioningInterceptor) { - defaultPartitioningInterceptor.setPartitionCount(partitions.size()); - } - }); - } - - KafkaTemplate kafkaTemplate = new KafkaTemplate<>(producerFB); - if (this.producerListener != null) { - kafkaTemplate.setProducerListener(this.producerListener); - } - if (transMan != null) { - kafkaTemplate.setTransactionIdPrefix(configurationProperties.getTransaction().getTransactionIdPrefix()); - } - final boolean allowNonTransactional = producerProperties.getExtension().isAllowNonTransactional(); - if (allowNonTransactional) { - kafkaTemplate.setAllowNonTransactional(allowNonTransactional); - } - ProducerConfigurationMessageHandler handler = new ProducerConfigurationMessageHandler( - kafkaTemplate, destination.getName(), producerProperties, producerFB, getBeanFactory()); - if (errorChannel != null) { - handler.setSendFailureChannel(errorChannel); - } - if (StringUtils.hasText(producerProperties.getExtension().getRecordMetadataChannel())) { - handler.setSendSuccessChannelName(producerProperties.getExtension().getRecordMetadataChannel()); - } - AbstractApplicationContext applicationContext = getApplicationContext(); - handler.setApplicationContext(applicationContext); - - KafkaHeaderMapper mapper = BindingUtils.getHeaderMapper(applicationContext, this.configurationProperties); - - /* - * Even if the user configures a bean, we must not use it if the header mode is - * not the default (headers); setting the mapper to null disables populating - * headers in the message handler. - */ - if (producerProperties.getHeaderMode() != null - && !HeaderMode.headers.equals(producerProperties.getHeaderMode())) { - mapper = null; - } - else if (mapper == null) { - String[] headerPatterns = producerProperties.getExtension().getHeaderPatterns(); - if (headerPatterns != null && headerPatterns.length > 0) { - mapper = new BinderHeaderMapper( - BinderHeaderMapper.addNeverHeaderPatterns(Arrays.asList(headerPatterns))); - } - else { - mapper = new BinderHeaderMapper(); - } - } - else { - KafkaHeaderMapper userHeaderMapper = mapper; - mapper = new KafkaHeaderMapper() { - - @Override - public void toHeaders(Headers source, Map target) { - userHeaderMapper.toHeaders(source, target); - } - - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - userHeaderMapper.fromHeaders(headers, target); - BinderHeaderMapper.removeNeverHeaders(target); - } - }; - - } - handler.setHeaderMapper(mapper); - - kafkaTemplate.setObservationEnabled(this.configurationProperties.isEnableObservation()); - kafkaTemplate.setKafkaAdmin(this.kafkaAdmin); - kafkaTemplate.setApplicationContext(getApplicationContext()); - - return handler; - } - - @Override - @SuppressWarnings("rawtypes") - protected void customizeProducerMessageHandler(MessageHandler producerMessageHandler, String destinationName) { - super.customizeProducerMessageHandler(producerMessageHandler, destinationName); - ((KafkaProducerMessageHandler) producerMessageHandler).getKafkaTemplate().afterSingletonsInstantiated(); - } - - @Override - protected void postProcessOutputChannel(MessageChannel outputChannel, - ExtendedProducerProperties producerProperties) { - - if (expressionInterceptorNeeded(producerProperties)) { - ((AbstractMessageChannel) outputChannel).addInterceptor(0, new KafkaExpressionEvaluatingInterceptor( - producerProperties.getExtension().getMessageKeyExpression(), getEvaluationContext())); - } - } - - private boolean expressionInterceptorNeeded( - ExtendedProducerProperties producerProperties) { - if (producerProperties.isUseNativeEncoding()) { - return false; // payload will be intact when it reaches the adapter - } - else { - Expression messageKeyExpression = producerProperties.getExtension().getMessageKeyExpression(); - return messageKeyExpression != null - && interceptorNeededPattern.matcher(messageKeyExpression.getExpressionString()).find(); - } - } - - protected DefaultKafkaProducerFactory getProducerFactory( - String transactionIdPrefix, - ExtendedProducerProperties producerProperties, String beanName, String destination) { - - Map props = BindingUtils.createProducerConfigs(producerProperties, - this.configurationProperties); - final KafkaProducerProperties kafkaProducerProperties = producerProperties.getExtension(); - this.producerConfigCustomizers - .forEach(customizer -> customizer.configure(props, producerProperties.getBindingName(), destination)); - DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>( - props); - if (transactionIdPrefix != null) { - producerFactory.setTransactionIdPrefix(transactionIdPrefix); - } - if (kafkaProducerProperties.getCloseTimeout() > 0) { - producerFactory.setPhysicalCloseTimeout(kafkaProducerProperties.getCloseTimeout()); - } - producerFactory.setBeanName(beanName); - this.clientFactoryCustomizers.forEach(customizer -> customizer.configure(producerFactory)); - return producerFactory; - } - - @Override - protected boolean useNativeEncoding( - ExtendedProducerProperties producerProperties) { - if (transactionManager(producerProperties.getExtension().getTransactionManager()) != null) { - return this.configurationProperties.getTransaction().getProducer() - .isUseNativeEncoding(); - } - return super.useNativeEncoding(producerProperties); - } - - @Override - @SuppressWarnings("unchecked") - protected MessageProducer createConsumerEndpoint( - final ConsumerDestination destination, final String group, - final ExtendedConsumerProperties extendedConsumerProperties) { - - boolean anonymous = !StringUtils.hasText(group); - Assert.isTrue( - !anonymous || !extendedConsumerProperties.getExtension().isEnableDlq(), - "DLQ support is not available for anonymous subscriptions"); - String consumerGroup = anonymous ? "anonymous." + UUID.randomUUID().toString() - : group; - final ConsumerFactory consumerFactory = createKafkaConsumerFactory( - anonymous, consumerGroup, extendedConsumerProperties, destination.getName() + ".consumer", destination.getName()); - int partitionCount = extendedConsumerProperties.getInstanceCount() - * extendedConsumerProperties.getConcurrency(); - - Collection listenedPartitions = new ArrayList<>(); - - boolean usingPatterns = extendedConsumerProperties.getExtension() - .isDestinationIsPattern(); - Assert.isTrue(!usingPatterns || !extendedConsumerProperties.isMultiplex(), - "Cannot use a pattern with multiplexed destinations; " - + "use the regex pattern to specify multiple topics instead"); - boolean groupManagement = extendedConsumerProperties.getExtension() - .isAutoRebalanceEnabled(); - if (!extendedConsumerProperties.isMultiplex()) { - listenedPartitions.addAll(processTopic(consumerGroup, - extendedConsumerProperties, consumerFactory, partitionCount, - usingPatterns, groupManagement, destination.getName())); - } - else { - for (String name : StringUtils - .commaDelimitedListToStringArray(destination.getName())) { - listenedPartitions.addAll(processTopic(consumerGroup, - extendedConsumerProperties, consumerFactory, partitionCount, - usingPatterns, groupManagement, name.trim())); - } - } - - String[] topics = extendedConsumerProperties.isMultiplex() - ? StringUtils.commaDelimitedListToStringArray(destination.getName()) - : new String[] { destination.getName() }; - for (int i = 0; i < topics.length; i++) { - topics[i] = topics[i].trim(); - } - if (!usingPatterns && !groupManagement) { - Assert.isTrue(!CollectionUtils.isEmpty(listenedPartitions), - "A list of partitions must be provided"); - } - final TopicPartitionOffset[] topicPartitionOffsets = groupManagement - ? null - : getTopicPartitionOffsets(listenedPartitions, extendedConsumerProperties, consumerFactory); - final ContainerProperties containerProperties = anonymous - || groupManagement - ? usingPatterns - ? new ContainerProperties(Pattern.compile(topics[0])) - : new ContainerProperties(topics) - : new ContainerProperties(topicPartitionOffsets); - - containerProperties.setObservationEnabled(this.configurationProperties.isEnableObservation()); - - KafkaAwareTransactionManager transMan = transactionManager( - extendedConsumerProperties.getExtension().getTransactionManager()); - if (transMan != null) { - containerProperties.setTransactionManager(transMan); - } - if (this.rebalanceListener != null) { - setupRebalanceListener(extendedConsumerProperties, containerProperties); - } - containerProperties.setIdleEventInterval( - extendedConsumerProperties.getExtension().getIdleEventInterval()); - int concurrency = usingPatterns ? extendedConsumerProperties.getConcurrency() - : Math.min(extendedConsumerProperties.getConcurrency(), - listenedPartitions.size()); - // in the event that auto rebalance is enabled, but no listened partitions are found - // we want to make sure that concurrency is a non-zero value. - if (groupManagement && listenedPartitions.isEmpty()) { - concurrency = extendedConsumerProperties.getConcurrency(); - } - resetOffsetsForAutoRebalance(extendedConsumerProperties, consumerFactory, containerProperties); - containerProperties.setAuthExceptionRetryInterval(this.configurationProperties.getAuthorizationExceptionRetryInterval()); - @SuppressWarnings("rawtypes") - final ConcurrentMessageListenerContainer messageListenerContainer = new ConcurrentMessageListenerContainer( - consumerFactory, containerProperties) { - - @Override - public void stop(Runnable callback) { - super.stop(callback); - } - - }; - - this.kafkaMessageListenerContainers.add(messageListenerContainer); - messageListenerContainer.setKafkaAdmin(this.kafkaAdmin); - messageListenerContainer.setConcurrency(concurrency); - // these won't be needed if the container is made a bean - AbstractApplicationContext applicationContext = getApplicationContext(); - messageListenerContainer.setApplicationContext(applicationContext); - if (getApplicationEventPublisher() != null) { - messageListenerContainer - .setApplicationEventPublisher(getApplicationEventPublisher()); - } - else if (applicationContext != null) { - messageListenerContainer - .setApplicationEventPublisher(applicationContext); - } - messageListenerContainer.setBeanName(destination + ".container"); - // end of these won't be needed... - ContainerProperties.AckMode ackMode = extendedConsumerProperties.getExtension().getAckMode(); - if (ackMode == null) { - if (extendedConsumerProperties.getExtension().isAckEachRecord()) { - ackMode = ContainerProperties.AckMode.RECORD; - } - else { - if (!extendedConsumerProperties.getExtension().isAutoCommitOffset()) { - messageListenerContainer.getContainerProperties() - .setAckMode(ContainerProperties.AckMode.MANUAL); - } - } - } - if (ackMode != null) { - if ((extendedConsumerProperties.isBatchMode() && ackMode != ContainerProperties.AckMode.RECORD) || - !extendedConsumerProperties.isBatchMode()) { - messageListenerContainer.getContainerProperties() - .setAckMode(ackMode); - } - } - - if (this.logger.isDebugEnabled()) { - this.logger.debug("Listened partitions: " - + StringUtils.collectionToCommaDelimitedString(listenedPartitions)); - } - final KafkaMessageDrivenChannelAdapter kafkaMessageDrivenChannelAdapter = - new KafkaMessageDrivenChannelAdapter<>(messageListenerContainer, - extendedConsumerProperties.isBatchMode() ? ListenerMode.batch : ListenerMode.record); - MessageConverter messageConverter = getMessageConverter(extendedConsumerProperties); - kafkaMessageDrivenChannelAdapter.setMessageConverter(messageConverter); - kafkaMessageDrivenChannelAdapter.setBeanFactory(getBeanFactory()); - kafkaMessageDrivenChannelAdapter.setApplicationContext(applicationContext); - ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(destination, - consumerGroup, extendedConsumerProperties); - - ListenerContainerCustomizer customizer = getContainerCustomizer(); - - if (!extendedConsumerProperties.isBatchMode() - && extendedConsumerProperties.getMaxAttempts() > 1 - && transMan == null) { - if (!(customizer instanceof ListenerContainerWithDlqAndRetryCustomizer c) - || c.retryAndDlqInBinding(destination.getName(), group)) { - kafkaMessageDrivenChannelAdapter - .setRetryTemplate(buildRetryTemplate(extendedConsumerProperties)); - kafkaMessageDrivenChannelAdapter - .setRecoveryCallback(errorInfrastructure.getRecoverer()); - } - if (!extendedConsumerProperties.getExtension().isEnableDlq()) { - messageListenerContainer.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(0L, 0L))); - } - } - else if (!extendedConsumerProperties.isBatchMode() && transMan != null) { - messageListenerContainer.setAfterRollbackProcessor(new DefaultAfterRollbackProcessor<>( - (record, exception) -> { - MessagingException payload = - new MessagingException(((RecordMessageConverter) messageConverter) - .toMessage(record, null, null, null), - "Transaction rollback limit exceeded", exception); - try { - errorInfrastructure.getErrorChannel() - .send(new ErrorMessage( - payload, - Collections.singletonMap(IntegrationMessageHeaderAccessor.SOURCE_DATA, - record))); - } - catch (Exception e) { - /* - * When there is no DLQ, the FinalRethrowingErrorMessageHandler will re-throw - * the payload; that will subvert the recovery and cause a re-seek of the failed - * record, so we ignore that here. - */ - if (!e.equals(payload)) { - throw e; - } - } - }, createBackOff(extendedConsumerProperties), - new KafkaTemplate<>(transMan.getProducerFactory()), - extendedConsumerProperties.getExtension().isTxCommitRecovered())); - } - else { - kafkaMessageDrivenChannelAdapter.setErrorChannel(errorInfrastructure.getErrorChannel()); - } - final String commonErrorHandlerBeanName = extendedConsumerProperties.getExtension().getCommonErrorHandlerBeanName(); - if (StringUtils.hasText(commonErrorHandlerBeanName)) { - final CommonErrorHandler commonErrorHandler = getApplicationContext().getBean(commonErrorHandlerBeanName, - CommonErrorHandler.class); - messageListenerContainer.setCommonErrorHandler(commonErrorHandler); - } - if (customizer instanceof ListenerContainerWithDlqAndRetryCustomizer c) { - - BiFunction, Exception, TopicPartition> destinationResolver = createDestResolver( - extendedConsumerProperties.getExtension()); - BackOff createBackOff = extendedConsumerProperties.getMaxAttempts() > 1 - ? createBackOff(extendedConsumerProperties) - : null; - c.configure(messageListenerContainer, destination.getName(), consumerGroup, destinationResolver, - createBackOff); - } - else { - ((ListenerContainerCustomizer) customizer) - .configure(messageListenerContainer, destination.getName(), consumerGroup); - } - this.ackModeInfo.put(destination, messageListenerContainer.getContainerProperties().getAckMode()); - return kafkaMessageDrivenChannelAdapter; - } - - private BiFunction, Exception, TopicPartition> createDestResolver( - KafkaConsumerProperties extension) { - - Integer dlqPartitions = extension.getDlqPartitions(); - if (extension.isEnableDlq()) { - return (rec, ex) -> dlqPartitions == null || dlqPartitions > 1 - ? new TopicPartition(extension.getDlqName(), rec.partition()) - : new TopicPartition(extension.getDlqName(), 0); - } - else { - return null; - } - } - - /** - * Configure a {@link BackOff} for the after rollback processor, based on the consumer - * retry properties. If retry is disabled, return a {@link BackOff} that disables - * retry. Otherwise use an {@link ExponentialBackOffWithMaxRetries}. - * @param extendedConsumerProperties the properties. - * @return the backoff. - */ - private BackOff createBackOff( - final ExtendedConsumerProperties extendedConsumerProperties) { - - int maxAttempts = extendedConsumerProperties.getMaxAttempts(); - if (maxAttempts < 2) { - return new FixedBackOff(0L, 0L); - } - int initialInterval = extendedConsumerProperties.getBackOffInitialInterval(); - int maxInterval = extendedConsumerProperties.getBackOffMaxInterval(); - double multiplier = extendedConsumerProperties.getBackOffMultiplier(); - ExponentialBackOff backOff = new ExponentialBackOffWithMaxRetries(maxAttempts - 1); - backOff.setInitialInterval(initialInterval); - backOff.setMaxInterval(maxInterval); - backOff.setMultiplier(multiplier); - return backOff; - } - - public void setupRebalanceListener( - final ExtendedConsumerProperties extendedConsumerProperties, - final ContainerProperties containerProperties) { - - Assert.isTrue(!extendedConsumerProperties.getExtension().isResetOffsets(), - "'resetOffsets' cannot be set when a KafkaBindingRebalanceListener is provided"); - final String bindingName = extendedConsumerProperties.getBindingName(); - Assert.notNull(bindingName, "'bindingName' cannot be null"); - final KafkaBindingRebalanceListener userRebalanceListener = this.rebalanceListener; - containerProperties - .setConsumerRebalanceListener(new ConsumerAwareRebalanceListener() { - - private final ThreadLocal initialAssignment = new ThreadLocal<>(); - - @Override - public void onPartitionsRevokedBeforeCommit(Consumer consumer, - Collection partitions) { - - userRebalanceListener.onPartitionsRevokedBeforeCommit(bindingName, - consumer, partitions); - } - - @Override - public void onPartitionsRevokedAfterCommit(Consumer consumer, - Collection partitions) { - - userRebalanceListener.onPartitionsRevokedAfterCommit(bindingName, - consumer, partitions); - } - - @Override - public void onPartitionsAssigned(Consumer consumer, - Collection partitions) { - try { - Boolean initial = this.initialAssignment.get(); - if (initial == null) { - initial = Boolean.TRUE; - } - userRebalanceListener.onPartitionsAssigned(bindingName, - consumer, partitions, initial); - } - finally { - this.initialAssignment.set(Boolean.FALSE); - } - } - - }); - } - - public Collection processTopic(final String group, - final ExtendedConsumerProperties extendedConsumerProperties, - final ConsumerFactory consumerFactory, int partitionCount, - boolean usingPatterns, boolean groupManagement, String topic) { - Collection listenedPartitions; - Collection allPartitions = usingPatterns ? Collections.emptyList() - : getPartitionInfo(topic, extendedConsumerProperties, consumerFactory, - partitionCount); - - if (groupManagement || extendedConsumerProperties.getInstanceCount() == 1) { - listenedPartitions = allPartitions; - } - else { - listenedPartitions = new ArrayList<>(); - for (PartitionInfo partition : allPartitions) { - // divide partitions across modules - if ((partition.partition() % extendedConsumerProperties - .getInstanceCount()) == extendedConsumerProperties - .getInstanceIndex()) { - listenedPartitions.add(partition); - } - } - } - this.topicsInUse.put(topic, - new TopicInformation(group, listenedPartitions, usingPatterns)); - return listenedPartitions; - } - - /* - * Reset the offsets if needed; may update the offsets in in the container's - * topicPartitionInitialOffsets. - */ - private void resetOffsetsForAutoRebalance( - final ExtendedConsumerProperties extendedConsumerProperties, - final ConsumerFactory consumerFactory, final ContainerProperties containerProperties) { - - final Object resetTo = checkReset(extendedConsumerProperties.getExtension().isResetOffsets(), - consumerFactory.getConfigurationProperties() - .get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)); - if (resetTo != null) { - Set sought = ConcurrentHashMap.newKeySet(); - containerProperties.setConsumerRebalanceListener(new ConsumerAwareRebalanceListener() { - - @Override - public void onPartitionsRevokedBeforeCommit( - Consumer consumer, Collection tps) { - - if (logger.isInfoEnabled()) { - logger.info("Partitions revoked: " + tps); - } - } - - @Override - public void onPartitionsRevokedAfterCommit( - Consumer consumer, Collection tps) { - // no op - } - - @Override - public void onPartitionsAssigned(Consumer consumer, Collection tps) { - if (logger.isInfoEnabled()) { - logger.info("Partitions assigned: " + tps); - } - List toSeek = tps.stream() - .filter(tp -> { - boolean shouldSeek = !sought.contains(tp); - sought.add(tp); - return shouldSeek; - }) - .collect(Collectors.toList()); - if (toSeek.size() > 0) { - if ("earliest".equals(resetTo)) { - consumer.seekToBeginning(toSeek); - } - else if ("latest".equals(resetTo)) { - consumer.seekToEnd(toSeek); - } - } - } - }); - } - } - - private Object checkReset(boolean resetOffsets, final Object resetTo) { - if (!resetOffsets) { - return null; - } - else if (!"earliest".equals(resetTo) && !"latest".equals(resetTo)) { - logger.warn("no (or unknown) " + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG - + " property cannot reset"); - return null; - } - else { - return resetTo; - } - } - - @Override - protected PolledConsumerResources createPolledConsumerResources(String name, - String group, ConsumerDestination destination, - ExtendedConsumerProperties extendedConsumerProperties) { - - boolean anonymous = !StringUtils.hasText(group); - final KafkaConsumerProperties extension = extendedConsumerProperties.getExtension(); - Assert.isTrue(!anonymous || !extension.isEnableDlq(), - "DLQ support is not available for anonymous subscriptions"); - String consumerGroup = anonymous ? "anonymous." + UUID.randomUUID().toString() - : group; - final ConsumerFactory consumerFactory = createKafkaConsumerFactory( - anonymous, consumerGroup, extendedConsumerProperties, destination.getName() + ".polled.consumer", - destination.getName()); - String[] topics = extendedConsumerProperties.isMultiplex() - ? StringUtils.commaDelimitedListToStringArray(destination.getName()) - : new String[] { destination.getName() }; - for (int i = 0; i < topics.length; i++) { - topics[i] = topics[i].trim(); - } - final ConsumerProperties consumerProperties = new ConsumerProperties(topics); - - String clientId = name; - if (extension.getConfiguration() - .containsKey(ConsumerConfig.CLIENT_ID_CONFIG)) { - clientId = extension.getConfiguration() - .get(ConsumerConfig.CLIENT_ID_CONFIG); - } - - consumerProperties.setClientId(clientId); - - consumerProperties.setConsumerRebalanceListener(new ConsumerRebalanceListener() { - - @Override - public void onPartitionsRevoked(Collection partitions) { - KafkaMessageChannelBinder.this.logger.info("Revoked: " + partitions); - } - - @Override - public void onPartitionsAssigned(Collection partitions) { - KafkaMessageChannelBinder.this.logger.info("Assigned: " + partitions); - } - - }); - - consumerProperties.setPollTimeout(extension.getPollTimeout()); - - KafkaMessageSource source = new KafkaMessageSource<>(consumerFactory, - consumerProperties); - MessageConverter messageConverter = getMessageConverter(extendedConsumerProperties); - Assert.isInstanceOf(RecordMessageConverter.class, messageConverter, - "'messageConverter' must be a 'RecordMessageConverter' for polled consumers"); - source.setMessageConverter((RecordMessageConverter) messageConverter); - source.setRawMessageHeader(extension.isEnableDlq()); - - if (!extendedConsumerProperties.isMultiplex()) { - // I copied this from the regular consumer - it looks bogus to me - includes - // all partitions - // not just the ones this binding is listening to; doesn't seem right for a - // health check. - Collection partitionInfos = getPartitionInfo( - destination.getName(), extendedConsumerProperties, consumerFactory, -1); - this.topicsInUse.put(destination.getName(), - new TopicInformation(consumerGroup, partitionInfos, false)); - } - else { - for (int i = 0; i < topics.length; i++) { - Collection partitionInfos = getPartitionInfo(topics[i], - extendedConsumerProperties, consumerFactory, -1); - this.topicsInUse.put(topics[i], - new TopicInformation(consumerGroup, partitionInfos, false)); - } - } - - getMessageSourceCustomizer().configure(source, destination.getName(), group); - return new PolledConsumerResources(source, registerErrorInfrastructure( - destination, group, extendedConsumerProperties, true)); - } - - @Override - protected void postProcessPollableSource(DefaultPollableMessageSource bindingTarget) { - bindingTarget.setAttributesProvider((accessor, message) -> { - Object rawMessage = message.getHeaders().get(KafkaHeaders.RAW_DATA); - if (rawMessage != null) { - accessor.setAttribute(KafkaHeaders.RAW_DATA, rawMessage); - } - }); - } - - private MessageConverter getMessageConverter( - final ExtendedConsumerProperties extendedConsumerProperties) { - - MessageConverter messageConverter = BindingUtils.getConsumerMessageConverter(getApplicationContext(), - extendedConsumerProperties, this.configurationProperties); - if (messageConverter instanceof MessagingMessageConverter messagingMessageConverter) { - messagingMessageConverter.setHeaderMapper(getHeaderMapper(extendedConsumerProperties)); - } - return messageConverter; - } - - private KafkaHeaderMapper getHeaderMapper( - final ExtendedConsumerProperties extendedConsumerProperties) { - - KafkaHeaderMapper mapper = BindingUtils.getHeaderMapper(getApplicationContext(), this.configurationProperties); - if (mapper == null) { - BinderHeaderMapper headerMapper = new BinderHeaderMapper() { - - @Override - public void toHeaders(Headers source, Map headers) { - super.toHeaders(source, headers); - if (headers.size() > 0) { - headers.put(BinderHeaders.NATIVE_HEADERS_PRESENT, Boolean.TRUE); - } - } - - }; - String[] trustedPackages = extendedConsumerProperties.getExtension() - .getTrustedPackages(); - if (!ObjectUtils.isEmpty(trustedPackages)) { - headerMapper.addTrustedPackages(trustedPackages); - } - mapper = headerMapper; - } - return mapper; - } - - private Collection getPartitionInfo(String topic, - final ExtendedConsumerProperties extendedConsumerProperties, - final ConsumerFactory consumerFactory, int partitionCount) { - return provisioningProvider.getPartitionsForTopic(partitionCount, - extendedConsumerProperties.getExtension().isAutoRebalanceEnabled(), - () -> { - try (Consumer consumer = consumerFactory.createConsumer()) { - return consumer.partitionsFor(topic); - } - }, topic); - } - - @Override - protected ErrorMessageStrategy getErrorMessageStrategy() { - return new RawRecordHeaderErrorMessageStrategy(); - } - - @SuppressWarnings("unchecked") - @Override - protected MessageHandler getErrorMessageHandler(final ConsumerDestination destination, - final String group, - final ExtendedConsumerProperties properties) { - - KafkaConsumerProperties kafkaConsumerProperties = properties.getExtension(); - if (kafkaConsumerProperties.isEnableDlq()) { - KafkaProducerProperties dlqProducerProperties = kafkaConsumerProperties - .getDlqProducerProperties(); - KafkaAwareTransactionManager transMan = transactionManager( - properties.getExtension().getTransactionManager()); - final ExtendedProducerProperties producerProperties = - new ExtendedProducerProperties<>(dlqProducerProperties); - producerProperties.populateBindingName(properties.getBindingName()); - ProducerFactory producerFactory = transMan != null - ? transMan.getProducerFactory() - : getProducerFactory(null, producerProperties, - destination.getName() + ".dlq.producer", destination.getName()); - final KafkaTemplate kafkaTemplate = new KafkaTemplate<>(producerFactory); - kafkaTemplate.setObservationEnabled(this.configurationProperties.isEnableObservation()); - kafkaTemplate.setKafkaAdmin(this.kafkaAdmin); - - Object timeout = producerFactory.getConfigurationProperties().get(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG); - Long sendTimeout = null; - if (timeout instanceof Number timeoutAsNumber) { - sendTimeout = timeoutAsNumber.longValue() + 2000L; - } - else if (timeout instanceof String timeoutAsString) { - sendTimeout = Long.parseLong(timeoutAsString) + 2000L; - } - if (timeout == null) { - sendTimeout = ((Integer) ProducerConfig.configDef() - .defaultValues() - .get(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG)).longValue() + 2000L; - } - @SuppressWarnings("rawtypes") - DlqSender dlqSender = new DlqSender(kafkaTemplate, sendTimeout); - - return (message) -> { - List> records; - if (!properties.isBatchMode()) { - ConsumerRecord record = StaticMessageHeaderAccessor.getSourceData(message); - records = List.of(Objects.requireNonNull(record)); - } - else { - records = StaticMessageHeaderAccessor.getSourceData(message); - } - if (!CollectionUtils.isEmpty(records)) { - records.forEach(record -> - handleRecordForDlq(record, destination, group, properties, kafkaConsumerProperties, - dlqProducerProperties, transMan, dlqSender, message)); - } - }; - } - return null; - } - - private void handleRecordForDlq(ConsumerRecord record, ConsumerDestination destination, String group, - ExtendedConsumerProperties properties, KafkaConsumerProperties kafkaConsumerProperties, - KafkaProducerProperties dlqProducerProperties, KafkaAwareTransactionManager transMan, - DlqSender dlqSender, Message message) { - if (properties.isUseNativeDecoding()) { - if (record != null) { - // Give the binder configuration the least preference. - Map configuration = this.configurationProperties.getConfiguration(); - // Then give any producer specific properties specified on the binder. - configuration.putAll(this.configurationProperties.getProducerProperties()); - Map configs = transMan == null - ? dlqProducerProperties.getConfiguration() - : this.configurationProperties.getTransaction() - .getProducer().getConfiguration(); - Assert.state(!configs.containsKey(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG), - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG + " cannot be overridden at the binding level; " - + "use multiple binders instead"); - // Finally merge with dlq producer properties or the transaction producer properties. - configuration.putAll(configs); - if (record.key() != null - && !record.key().getClass().isInstance(byte[].class)) { - ensureDlqMessageCanBeProperlySerialized(configuration, - (Map config) -> !config - .containsKey("key.serializer"), - "Key"); - } - if (record.value() != null - && !record.value().getClass().isInstance(byte[].class)) { - ensureDlqMessageCanBeProperlySerialized(configuration, - (Map config) -> !config - .containsKey("value.serializer"), - "Payload"); - } - } - } - - if (record == null) { - this.logger.error("No raw record; cannot send to DLQ: " + message); - return; - } - Headers kafkaHeaders = new RecordHeaders(record.headers().toArray()); - AtomicReference> recordToSend = new AtomicReference<>( - record); - Throwable throwable = null; - if (message.getPayload() instanceof Throwable throwablePayload) { - - throwable = throwablePayload; - - HeaderMode headerMode = properties.getHeaderMode(); - - if (headerMode == null || HeaderMode.headers.equals(headerMode)) { - - kafkaHeaders.add(new RecordHeader(X_ORIGINAL_TOPIC, - record.topic().getBytes(StandardCharsets.UTF_8))); - kafkaHeaders.add(new RecordHeader(X_ORIGINAL_PARTITION, - ByteBuffer.allocate(Integer.BYTES) - .putInt(record.partition()).array())); - kafkaHeaders.add(new RecordHeader(X_ORIGINAL_OFFSET, ByteBuffer - .allocate(Long.BYTES).putLong(record.offset()).array())); - kafkaHeaders.add(new RecordHeader(X_ORIGINAL_TIMESTAMP, - ByteBuffer.allocate(Long.BYTES) - .putLong(record.timestamp()).array())); - kafkaHeaders.add(new RecordHeader(X_ORIGINAL_TIMESTAMP_TYPE, - record.timestampType().toString() - .getBytes(StandardCharsets.UTF_8))); - kafkaHeaders.add(new RecordHeader(X_EXCEPTION_FQCN, throwable - .getClass().getName().getBytes(StandardCharsets.UTF_8))); - String exceptionMessage = throwable.getMessage(); - if (exceptionMessage != null) { - kafkaHeaders.add(new RecordHeader(X_EXCEPTION_MESSAGE, - exceptionMessage.getBytes(StandardCharsets.UTF_8))); - } - kafkaHeaders.add(new RecordHeader(X_EXCEPTION_STACKTRACE, - getStackTraceAsString(throwable) - .getBytes(StandardCharsets.UTF_8))); - } - else if (HeaderMode.embeddedHeaders.equals(headerMode)) { - try { - MessageValues messageValues = EmbeddedHeaderUtils - .extractHeaders(MessageBuilder - .withPayload((byte[]) record.value()).build(), - false); - messageValues.put(X_ORIGINAL_TOPIC, record.topic()); - messageValues.put(X_ORIGINAL_PARTITION, record.partition()); - messageValues.put(X_ORIGINAL_OFFSET, record.offset()); - messageValues.put(X_ORIGINAL_TIMESTAMP, record.timestamp()); - messageValues.put(X_ORIGINAL_TIMESTAMP_TYPE, - record.timestampType().toString()); - messageValues.put(X_EXCEPTION_FQCN, - throwable.getClass().getName()); - messageValues.put(X_EXCEPTION_MESSAGE, - throwable.getMessage()); - messageValues.put(X_EXCEPTION_STACKTRACE, - getStackTraceAsString(throwable)); - - final String[] headersToEmbed = new ArrayList<>( - messageValues.keySet()).toArray( - new String[messageValues.keySet().size()]); - byte[] payload = EmbeddedHeaderUtils.embedHeaders( - messageValues, - EmbeddedHeaderUtils.headersToEmbed(headersToEmbed)); - recordToSend.set(new ConsumerRecord( - record.topic(), record.partition(), record.offset(), - record.key(), payload)); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - } - - MessageHeaders headers; - if (message instanceof ErrorMessage errorMessage) { - final Message originalMessage = errorMessage.getOriginalMessage(); - if (originalMessage != null) { - headers = originalMessage.getHeaders(); - } - else { - headers = message.getHeaders(); - } - } - else { - headers = message.getHeaders(); - } - String dlqName = this.dlqDestinationResolver != null ? - this.dlqDestinationResolver.apply(recordToSend.get(), new Exception(throwable)) : StringUtils.hasText(kafkaConsumerProperties.getDlqName()) - ? kafkaConsumerProperties.getDlqName() - : "error." + record.topic() + "." + group; - if (this.transactionTemplate != null) { - Throwable throwable2 = throwable; - this.transactionTemplate.executeWithoutResult(status -> { - dlqSender.sendToDlq(recordToSend.get(), kafkaHeaders, dlqName, group, throwable2, - determinDlqPartitionFunction(properties.getExtension().getDlqPartitions()), - headers, this.ackModeInfo.get(destination)); - }); - } - else { - dlqSender.sendToDlq(recordToSend.get(), kafkaHeaders, dlqName, group, throwable, - determinDlqPartitionFunction(properties.getExtension().getDlqPartitions()), headers, this.ackModeInfo.get(destination)); - } - } - - @SuppressWarnings("unchecked") - @Nullable - private KafkaAwareTransactionManager transactionManager(@Nullable String beanName) { - if (StringUtils.hasText(beanName)) { - return getApplicationContext().getBean(beanName, KafkaAwareTransactionManager.class); - } - return this.transactionManager; - } - - private DlqPartitionFunction determinDlqPartitionFunction(Integer dlqPartitions) { - if (this.dlqPartitionFunction != null) { - return this.dlqPartitionFunction; - } - else { - return DlqPartitionFunction.determineFallbackFunction(dlqPartitions, this.logger); - } - } - - @Override - protected MessageHandler getPolledConsumerErrorMessageHandler( - ConsumerDestination destination, String group, - ExtendedConsumerProperties properties) { - if (properties.getExtension().isEnableDlq()) { - return getErrorMessageHandler(destination, group, properties); - } - final MessageHandler superHandler = super.getErrorMessageHandler(destination, - group, properties); - return (message) -> { - ConsumerRecord record = (ConsumerRecord) message.getHeaders() - .get(KafkaHeaders.RAW_DATA); - if (!(message instanceof ErrorMessage)) { - logger.error("Expected an ErrorMessage, not a " - + message.getClass().toString() + " for: " + message); - } - else if (record == null) { - if (superHandler != null) { - superHandler.handleMessage(message); - } - } - else { - if (message.getPayload() instanceof MessagingException messagingException) { - AcknowledgmentCallback ack = StaticMessageHeaderAccessor - .getAcknowledgmentCallback(messagingException.getFailedMessage()); - if (ack != null) { - if (isAutoCommitOnError(properties)) { - ack.acknowledge(AcknowledgmentCallback.Status.REJECT); - } - else { - ack.acknowledge(AcknowledgmentCallback.Status.REQUEUE); - } - } - } - } - }; - } - - private static void ensureDlqMessageCanBeProperlySerialized( - Map configuration, - Predicate> configPredicate, String dataType) { - if (CollectionUtils.isEmpty(configuration) - || configPredicate.test(configuration)) { - throw new IllegalArgumentException("Native decoding is used on the consumer. " - + dataType - + " is not byte[] and no serializer is set on the DLQ producer."); - } - } - - protected ConsumerFactory createKafkaConsumerFactory(boolean anonymous, - String consumerGroup, ExtendedConsumerProperties consumerProperties, - String beanName, String destination) { - - Map props = BindingUtils.createConsumerConfigs(anonymous, consumerGroup, consumerProperties, - this.configurationProperties); - - this.consumerConfigCustomizers - .forEach(customizer -> customizer.configure(props, consumerProperties.getBindingName(), destination)); - DefaultKafkaConsumerFactory factory = new DefaultKafkaConsumerFactory<>(props); - factory.setBeanName(beanName); - this.clientFactoryCustomizers.forEach(customizer -> customizer.configure(factory)); - return factory; - } - - private boolean isAutoCommitOnError( - ExtendedConsumerProperties properties) { - return properties.getExtension().getAutoCommitOnError() != null - ? properties.getExtension().getAutoCommitOnError() - : false; - } - - private TopicPartitionOffset[] getTopicPartitionOffsets( - Collection listenedPartitions, - ExtendedConsumerProperties extendedConsumerProperties, - ConsumerFactory consumerFactory) { - - final TopicPartitionOffset[] TopicPartitionOffsets = - new TopicPartitionOffset[listenedPartitions.size()]; - int i = 0; - SeekPosition seekPosition = null; - Object resetTo = checkReset(extendedConsumerProperties.getExtension().isResetOffsets(), - consumerFactory.getConfigurationProperties().get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)); - if (resetTo != null) { - seekPosition = "earliest".equals(resetTo) ? SeekPosition.BEGINNING : SeekPosition.END; - } - for (PartitionInfo partition : listenedPartitions) { - - TopicPartitionOffsets[i++] = new TopicPartitionOffset( - partition.topic(), partition.partition(), seekPosition); - } - return TopicPartitionOffsets; - } - - private String toDisplayString(String original, int maxCharacters) { - if (original.length() <= maxCharacters) { - return original; - } - return original.substring(0, maxCharacters) + "..."; - } - - private String getStackTraceAsString(Throwable cause) { - StringWriter stringWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stringWriter, true); - cause.printStackTrace(printWriter); - return stringWriter.getBuffer().toString(); - } - - /** - * Set a {@link ConsumerConfigCustomizer} for the {@link ConsumerFactory} created inside the binder. - * @param consumerConfigCustomizer the consumer config customizer - * @deprecated in favor of {@link #addConsumerConfigCustomizer(ConsumerConfigCustomizer)}. - */ - @Deprecated - public void setConsumerConfigCustomizer(ConsumerConfigCustomizer consumerConfigCustomizer) { - addConsumerConfigCustomizer(consumerConfigCustomizer); - } - - public void addConsumerConfigCustomizer(ConsumerConfigCustomizer consumerConfigCustomizer) { - if (consumerConfigCustomizer != null) { - this.consumerConfigCustomizers.add(consumerConfigCustomizer); - } - } - - /** - * Set a {@link ProducerConfigCustomizer} for the {@link ProducerFactory} created inside the binder. - * @param producerConfigCustomizer the producer config customizer - * @deprecated in favor of {@link #addProducerConfigCustomizer(ProducerConfigCustomizer)}. - */ - @Deprecated - public void setProducerConfigCustomizer(ProducerConfigCustomizer producerConfigCustomizer) { - addProducerConfigCustomizer(producerConfigCustomizer); - } - - public void addProducerConfigCustomizer(ProducerConfigCustomizer producerConfigCustomizer) { - if (producerConfigCustomizer != null) { - this.producerConfigCustomizers.add(producerConfigCustomizer); - } - } - - List> getKafkaMessageListenerContainers() { - return Collections.unmodifiableList(kafkaMessageListenerContainers); - } - - final class ProducerConfigurationMessageHandler - extends KafkaProducerMessageHandler { - - private boolean running = true; - - private final ProducerFactory producerFactory; - - PartitionHandler kafkaPartitionHandler = null; - - private String topic; - - ProducerConfigurationMessageHandler(KafkaTemplate kafkaTemplate, - String topic, - ExtendedProducerProperties producerProperties, - ProducerFactory producerFactory, ConfigurableListableBeanFactory beanFactory) { - - super(kafkaTemplate); - this.topic = topic; - - if (producerProperties.getExtension().isUseTopicHeader()) { - setTopicExpression(PARSER.parseExpression("headers['" + KafkaHeaders.TOPIC + "'] ?: '" + topic + "'")); - } - else { - setTopicExpression(new LiteralExpression(topic)); - } - Expression messageKeyExpression = producerProperties.getExtension().getMessageKeyExpression(); - if (expressionInterceptorNeeded(producerProperties)) { - messageKeyExpression = PARSER.parseExpression("headers['" - + KafkaExpressionEvaluatingInterceptor.MESSAGE_KEY_HEADER - + "']"); - } - setMessageKeyExpression(messageKeyExpression); - setBeanFactory(KafkaMessageChannelBinder.this.getBeanFactory()); - if (producerProperties.isPartitioned()) { - setPartitionIdExpression(PARSER.parseExpression( - "headers['" + BinderHeaders.PARTITION_HEADER + "']")); - } - if (producerProperties.getExtension().isSync()) { - setSync(true); - } - if (producerProperties.getExtension().getSendTimeoutExpression() != null) { - setSendTimeoutExpression(producerProperties.getExtension().getSendTimeoutExpression()); - } - this.producerFactory = producerFactory; - - /* - Activate own instance of a PartitionHandler if necessary/possible to override any other existing - partition calculation (see other usages of PartitionHandler) by using current partition count - (which may have changed at runtime) each time a message is handled. - PartitionKeyExpression 'payload' is not supported here, because of - OutboundContentTypeConvertingInterceptor would have been called before and the payload will be encoded and - not readable for PartitionHandler during handleMessage method. - */ - if (producerProperties.isDynamicPartitionUpdatesEnabled() && - producerProperties.getPartitionKeyExpression() != null && - !(producerProperties.getPartitionKeyExpression().getExpressionString() - .toLowerCase().contains("payload"))) { - kafkaPartitionHandler = - new PartitionHandler(ExpressionUtils.createStandardEvaluationContext(beanFactory), - producerProperties, beanFactory); - } - } - - @Override - public void start() { - try { - super.onInit(); - } - catch (Exception ex) { - this.logger.error(ex, "Initialization errors: "); - throw new RuntimeException(ex); - } - } - - @Override - public void stop() { - if (this.producerFactory instanceof DisposableBean disposableProducerFactory) { - try { - disposableProducerFactory.destroy(); - } - catch (Exception ex) { - this.logger.error(ex, "Error destroying the producer factory bean: "); - throw new RuntimeException(ex); - } - } - this.running = false; - } - - @Override - public boolean isRunning() { - return this.running; - } - - @Override - public void handleMessage(Message message) { - - // if we use our own partition handler to update partition count we recalculate partition - if (kafkaPartitionHandler != null) { - kafkaPartitionHandler.setPartitionCount(getKafkaTemplate().partitionsFor(this.topic).size()); - int partitionId = kafkaPartitionHandler.determinePartition(message); - - Message newMessage = MessageBuilder - .fromMessage(message) - .setHeader(BinderHeaders.PARTITION_HEADER, partitionId).build(); - - super.handleMessage(newMessage); - } - else { - super.handleMessage(message); - } - } - } - - /** - * Helper class to send to DLQ. - * - * @param generic type for key - * @param generic type for value - */ - private final class DlqSender { - - private final KafkaTemplate kafkaTemplate; - - private final long sendTimeout; - - DlqSender(KafkaTemplate kafkaTemplate, long timeout) { - this.kafkaTemplate = kafkaTemplate; - this.sendTimeout = timeout; - } - - @SuppressWarnings("unchecked") - void sendToDlq(ConsumerRecord consumerRecord, Headers headers, - String dlqName, String group, Throwable throwable, DlqPartitionFunction partitionFunction, - MessageHeaders messageHeaders, ContainerProperties.AckMode ackMode) { - K key = (K) consumerRecord.key(); - V value = (V) consumerRecord.value(); - ProducerRecord producerRecord = new ProducerRecord<>(dlqName, - partitionFunction.apply(group, consumerRecord, throwable), - key, value, headers); - - StringBuilder sb = new StringBuilder().append(" a message with key='") - .append(keyOrValue(key)) - .append("'").append(" and payload='") - .append(keyOrValue(value)) - .append("'").append(" received from ") - .append(consumerRecord.partition()); - CompletableFuture> sentDlq = null; - try { - sentDlq = this.kafkaTemplate.send(producerRecord); - sentDlq.whenComplete((result, ex) -> { - if (ex != null) { - KafkaMessageChannelBinder.this.logger.error("Error sending to DLQ " + sb, ex); - } - else { - if (KafkaMessageChannelBinder.this.logger.isDebugEnabled()) { - KafkaMessageChannelBinder.this.logger.debug("Sent to DLQ " + sb + ": " + result.getRecordMetadata()); - } - } - }); - try { - sentDlq.get(this.sendTimeout, TimeUnit.MILLISECONDS); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - throw ex; - } - } - catch (Exception ex) { - KafkaMessageChannelBinder.this.logger - .error("Error sending to DLQ " + sb.toString(), ex); - } - finally { - if (ackMode == ContainerProperties.AckMode.MANUAL - || ackMode == ContainerProperties.AckMode.MANUAL_IMMEDIATE) { - messageHeaders.get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class).acknowledge(); - } - } - } - - private String keyOrValue(Object keyOrValue) { - if (keyOrValue instanceof byte[] keyOrValueBytes) { - return "byte[" + keyOrValueBytes.length + "]"; - } - else { - return toDisplayString(ObjectUtils.nullSafeToString(keyOrValue), 50); - } - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaNullConverter.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaNullConverter.java deleted file mode 100644 index 713dbf338..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaNullConverter.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka; - -import java.util.Collections; - -import org.springframework.kafka.support.KafkaNull; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.AbstractMessageConverter; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.util.MimeTypeUtils; - -/** - * A {@link MessageConverter} that supports {@link KafkaNull} payloads. - * - * @author Gary Russell - * @author Aldo Sinanaj - * @since 2.2 - */ -public class KafkaNullConverter extends AbstractMessageConverter { - - public KafkaNullConverter() { - super(Collections.singletonList(MimeTypeUtils.ALL)); - } - - @Override - protected boolean supportsMimeType(MessageHeaders headers) { - return true; - } - - @Override - protected boolean supports(Class aClass) { - return KafkaNull.class.equals(aClass); - } - - @Override - protected boolean canConvertFrom(Message message, Class targetClass) { - return message.getPayload() instanceof KafkaNull; - } - - @Override - protected Object convertFromInternal(Message message, Class targetClass, - Object conversionHint) { - return message.getPayload(); - } - - @Override - protected Object convertToInternal(Object payload, MessageHeaders headers, - Object conversionHint) { - return payload; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/ListenerContainerWithDlqAndRetryCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/ListenerContainerWithDlqAndRetryCustomizer.java deleted file mode 100644 index ed453c097..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/ListenerContainerWithDlqAndRetryCustomizer.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import java.util.function.BiFunction; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.TopicPartition; - -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.lang.Nullable; -import org.springframework.util.backoff.BackOff; - -/** - * An extension of {@link ListenerContainerCustomizer} that provides access to dead letter - * metadata. - * - * @author Gary Russell - * @since 3.2 - * - */ -public interface ListenerContainerWithDlqAndRetryCustomizer - extends ListenerContainerCustomizer> { - - @Override - default void configure(AbstractMessageListenerContainer container, String destinationName, String group) { - } - - /** - * Configure the container. - * @param container the container. - * @param destinationName the destination name. - * @param group the group. - * @param dlqDestinationResolver a destination resolver for the dead letter topic (if - * enableDlq). - * @param backOff the backOff using retry properties (if configured). - * @see #retryAndDlqInBinding(String, String) - */ - void configure(AbstractMessageListenerContainer container, String destinationName, String group, - @Nullable BiFunction, Exception, TopicPartition> dlqDestinationResolver, - @Nullable BackOff backOff); - - /** - * Return false to move retries and DLQ from the binding to a customized error handler - * using the retry metadata and/or a {@code DeadLetterPublishingRecoverer} when - * configured via - * {@link #configure(AbstractMessageListenerContainer, String, String, BiFunction, BackOff)}. - * @param destinationName the destination name. - * @param group the group. - * @return false to disable retries and DLQ in the binding - */ - default boolean retryAndDlqInBinding(String destinationName, String group) { - return true; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/ClientFactoryCustomizer.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/ClientFactoryCustomizer.java deleted file mode 100644 index d2583dc5a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/ClientFactoryCustomizer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.config; - -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.ProducerFactory; - -/** - * Called by the binder to customize the factories. - * - * @author Gary Russell - * @since 3.0.6 - * - */ -public interface ClientFactoryCustomizer { - - default void configure(ProducerFactory pf) { - } - - default void configure(ConsumerFactory cf) { - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/ExtendedBindingHandlerMappingsProviderConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/ExtendedBindingHandlerMappingsProviderConfiguration.java deleted file mode 100644 index 2a3d8e222..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/ExtendedBindingHandlerMappingsProviderConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.config; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.cloud.stream.config.BindingHandlerAdvise.MappingsProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration for extended binding metadata. - * - * @author Oleg Zhurakousky - */ - -@Configuration(proxyBeanMethods = false) -public class ExtendedBindingHandlerMappingsProviderConfiguration { - - @Bean - public MappingsProvider kafkaExtendedPropertiesDefaultMappingsProvider() { - return () -> { - Map mappings = new HashMap<>(); - mappings.put( - ConfigurationPropertyName.of("spring.cloud.stream.kafka.bindings"), - ConfigurationPropertyName.of("spring.cloud.stream.kafka.default")); - return mappings; - }; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/KafkaBinderConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/KafkaBinderConfiguration.java deleted file mode 100644 index 0d43aabe4..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/KafkaBinderConfiguration.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.config; - -import java.io.IOException; -import java.util.stream.Collectors; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.MeterBinder; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.kafka.KafkaBinderMetrics; -import org.springframework.cloud.stream.binder.kafka.KafkaBindingRebalanceListener; -import org.springframework.cloud.stream.binder.kafka.KafkaMessageChannelBinder; -import org.springframework.cloud.stream.binder.kafka.KafkaNullConverter; -import org.springframework.cloud.stream.binder.kafka.properties.JaasLoginModuleConfiguration; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaExtendedBindingProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.support.ConsumerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.utils.DlqDestinationResolver; -import org.springframework.cloud.stream.binder.kafka.utils.DlqPartitionFunction; -import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter; -import org.springframework.integration.kafka.inbound.KafkaMessageSource; -import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.MicrometerConsumerListener; -import org.springframework.kafka.core.MicrometerProducerListener; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.kafka.security.jaas.KafkaJaasLoginModuleInitializer; -import org.springframework.kafka.support.LoggingProducerListener; -import org.springframework.kafka.support.ProducerListener; -import org.springframework.lang.Nullable; -import org.springframework.messaging.converter.MessageConverter; - -/** - * Kafka binder configuration class. - * - * @author David Turanski - * @author Marius Bogoevici - * @author Soby Chacko - * @author Mark Fisher - * @author Ilayaperumal Gopinathan - * @author Henryk Konsek - * @author Gary Russell - * @author Oleg Zhurakousky - * @author Artem Bilan - * @author Aldo Sinanaj - * @author Chris Bono - * @author Yi Liu - * @author Byungjun You - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(Binder.class) -@Import({ KafkaBinderHealthIndicatorConfiguration.class }) -@EnableConfigurationProperties({ KafkaProperties.class, KafkaExtendedBindingProperties.class }) -public class KafkaBinderConfiguration { - - /** - * @ConfigurationProperties is declared on the @Bean method for Spring Boot to ignore - * constructor binding on KafkaBinderConfigurationProperties. If constructor binding is - * used, it ignores all the JavaBeans style properties when generating configuration metadata. - * - * See the following issues for more details: - * - * https://github.com/spring-cloud/spring-cloud-stream/issues/2640 - * https://github.com/spring-projects/spring-boot/issues/34031 - * - * @param kafkaProperties Spring Kafka properties autoconfigured by Spring Boot - */ - @Bean - @ConfigurationProperties(prefix = "spring.cloud.stream.kafka.binder") - KafkaBinderConfigurationProperties configurationProperties( - KafkaProperties kafkaProperties) { - return new KafkaBinderConfigurationProperties(kafkaProperties); - } - - @Bean - KafkaTopicProvisioner provisioningProvider( - KafkaBinderConfigurationProperties configurationProperties, - ObjectProvider adminClientConfigCustomizers, KafkaProperties kafkaProperties) { - return new KafkaTopicProvisioner(configurationProperties, - kafkaProperties, adminClientConfigCustomizers.orderedStream().collect(Collectors.toList())); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Bean - KafkaMessageChannelBinder kafkaMessageChannelBinder( - KafkaBinderConfigurationProperties configurationProperties, - KafkaTopicProvisioner provisioningProvider, - @Nullable ListenerContainerCustomizer> listenerContainerCustomizer, - @Nullable MessageSourceCustomizer> sourceCustomizer, - @Nullable ProducerMessageHandlerCustomizer> messageHandlerCustomizer, - @Nullable ConsumerEndpointCustomizer> consumerCustomizer, - ObjectProvider rebalanceListener, - ObjectProvider dlqPartitionFunction, - ObjectProvider dlqDestinationResolver, - ObjectProvider clientFactoryCustomizers, - ObjectProvider consumerConfigCustomizers, - ObjectProvider producerConfigCustomizers, - ProducerListener producerListener, KafkaExtendedBindingProperties kafkaExtendedBindingProperties - ) { - - KafkaMessageChannelBinder kafkaMessageChannelBinder = new KafkaMessageChannelBinder( - configurationProperties, provisioningProvider, - listenerContainerCustomizer, sourceCustomizer, rebalanceListener.getIfUnique(), - dlqPartitionFunction.getIfUnique(), dlqDestinationResolver.getIfUnique()); - kafkaMessageChannelBinder.setProducerListener(producerListener); - kafkaMessageChannelBinder - .setExtendedBindingProperties(kafkaExtendedBindingProperties); - kafkaMessageChannelBinder.setProducerMessageHandlerCustomizer(messageHandlerCustomizer); - kafkaMessageChannelBinder.setConsumerEndpointCustomizer(consumerCustomizer); - clientFactoryCustomizers.orderedStream().forEach(kafkaMessageChannelBinder::addClientFactoryCustomizer); - consumerConfigCustomizers.orderedStream().forEach(kafkaMessageChannelBinder::addConsumerConfigCustomizer); - producerConfigCustomizers.orderedStream().forEach(kafkaMessageChannelBinder::addProducerConfigCustomizer); - return kafkaMessageChannelBinder; - } - - @SuppressWarnings("rawtypes") - @Bean - @ConditionalOnMissingBean(ProducerListener.class) - ProducerListener producerListener() { - return new LoggingProducerListener(); - } - - @Bean - @ConditionalOnMissingBean(KafkaNullConverter.class) - MessageConverter kafkaNullConverter() { - return new KafkaNullConverter(); - } - - @Bean - @ConditionalOnMissingBean(KafkaJaasLoginModuleInitializer.class) - public KafkaJaasLoginModuleInitializer jaasInitializer( - KafkaBinderConfigurationProperties configurationProperties) - throws IOException { - JaasLoginModuleConfiguration jaas = configurationProperties.getJaas(); - if (jaas == null) { - return null; - } - else { - KafkaJaasLoginModuleInitializer kafkaJaasLoginModuleInitializer = new KafkaJaasLoginModuleInitializer(); - kafkaJaasLoginModuleInitializer.setLoginModule(jaas.getLoginModule()); - - KafkaJaasLoginModuleInitializer.ControlFlag controlFlag = jaas - .getControlFlag(); - - if (controlFlag != null) { - kafkaJaasLoginModuleInitializer.setControlFlag(controlFlag); - } - kafkaJaasLoginModuleInitializer.setOptions(jaas.getOptions()); - return kafkaJaasLoginModuleInitializer; - } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(value = KafkaBinderMetrics.class, name = "outerContext") - @ConditionalOnClass(name = "io.micrometer.core.instrument.MeterRegistry") - protected class KafkaBinderMetricsConfiguration { - - @Bean - @ConditionalOnBean(MeterRegistry.class) - @ConditionalOnMissingBean(KafkaBinderMetrics.class) - public MeterBinder kafkaBinderMetrics( - KafkaMessageChannelBinder kafkaMessageChannelBinder, - KafkaBinderConfigurationProperties configurationProperties, - MeterRegistry meterRegistry) { - - return new KafkaBinderMetrics(kafkaMessageChannelBinder, - configurationProperties, null, meterRegistry); - } - - @ConditionalOnClass(name = "org.springframework.kafka.core.MicrometerConsumerListener") - @ConditionalOnBean(MeterRegistry.class) - protected class KafkaMicrometer { - - @Bean - @ConditionalOnMissingBean(name = "binderClientFactoryCustomizer") - public ClientFactoryCustomizer binderClientFactoryCustomizer(MeterRegistry meterRegistry) { - - return new ClientFactoryCustomizer() { - - @Override - public void configure(ProducerFactory pf) { - if (pf instanceof DefaultKafkaProducerFactory) { - pf.addListener(new MicrometerProducerListener<>(meterRegistry)); - } - } - - @Override - public void configure(ConsumerFactory cf) { - if (cf instanceof DefaultKafkaConsumerFactory) { - cf.addListener(new MicrometerConsumerListener<>(meterRegistry)); - } - } - - }; - - } - - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(name = "outerContext") - @ConditionalOnMissingBean(KafkaBinderMetrics.class) - @ConditionalOnClass(name = "io.micrometer.core.instrument.MeterRegistry") - protected class KafkaBinderMetricsConfigurationWithMultiBinder { - - @Bean - public MeterBinder kafkaBinderMetrics( - KafkaMessageChannelBinder kafkaMessageChannelBinder, - KafkaBinderConfigurationProperties configurationProperties, - ConfigurableApplicationContext context) { - - MeterRegistry meterRegistry = context.getBean("outerContext", ApplicationContext.class) - .getBean(MeterRegistry.class); - return new KafkaBinderMetrics(kafkaMessageChannelBinder, - configurationProperties, null, meterRegistry); - } - - @ConditionalOnClass(name = "org.springframework.kafka.core.MicrometerConsumerListener") - protected class KafkaMicrometer { - - @Bean - @ConditionalOnMissingBean(name = "binderClientFactoryCustomizer") - public ClientFactoryCustomizer binderClientFactoryCustomizer(ConfigurableApplicationContext context) { - - - return new ClientFactoryCustomizer() { - - MeterRegistry meterRegistry = context.getBean("outerContext", ApplicationContext.class) - .getBean(MeterRegistry.class); - - @Override - public void configure(ProducerFactory pf) { - if (pf instanceof DefaultKafkaProducerFactory) { - pf.addListener(new MicrometerProducerListener<>(this.meterRegistry)); - } - } - - @Override - public void configure(ConsumerFactory cf) { - if (cf instanceof DefaultKafkaConsumerFactory) { - cf.addListener(new MicrometerConsumerListener<>(this.meterRegistry)); - } - } - - }; - - } - - } - - } - - /** - * Properties configuration for Jaas. - */ - @SuppressWarnings("unused") - public static class JaasConfigurationProperties { - - private JaasLoginModuleConfiguration kafka; - - private JaasLoginModuleConfiguration zookeeper; - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/KafkaBinderHealthIndicatorConfiguration.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/KafkaBinderHealthIndicatorConfiguration.java deleted file mode 100644 index 67d53a182..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/config/KafkaBinderHealthIndicatorConfiguration.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.config; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.common.serialization.ByteArrayDeserializer; - -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.stream.binder.kafka.KafkaBinderHealth; -import org.springframework.cloud.stream.binder.kafka.KafkaBinderHealthIndicator; -import org.springframework.cloud.stream.binder.kafka.KafkaMessageChannelBinder; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.util.ObjectUtils; - -/** - * Configuration class for Kafka binder health indicator beans. - * - * @author Oleg Zhurakousky - * @author Chukwubuikem Ume-Ugwa - * @author Soby Chacko - */ - -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") -@ConditionalOnEnabledHealthIndicator("binders") -@ConditionalOnMissingBean(KafkaBinderHealth.class) -public class KafkaBinderHealthIndicatorConfiguration { - - @Bean - public KafkaBinderHealthIndicator kafkaBinderHealthIndicator( - KafkaMessageChannelBinder kafkaMessageChannelBinder, - KafkaBinderConfigurationProperties configurationProperties) { - Map props = new HashMap<>(); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - Map mergedConfig = configurationProperties - .mergedConsumerConfiguration(); - if (!ObjectUtils.isEmpty(mergedConfig)) { - props.putAll(mergedConfig); - } - if (!props.containsKey(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG)) { - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, - configurationProperties.getKafkaConnectionString()); - } - ConsumerFactory consumerFactory = new DefaultKafkaConsumerFactory<>(props); - KafkaBinderHealthIndicator indicator = new KafkaBinderHealthIndicator( - kafkaMessageChannelBinder, consumerFactory); - indicator.setTimeout(configurationProperties.getHealthTimeout()); - indicator.setConsiderDownWhenAnyPartitionHasNoLeader(configurationProperties.isConsiderDownWhenAnyPartitionHasNoLeader()); - return indicator; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring.binders b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring.binders deleted file mode 100644 index 063a7400f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -kafka:\ -org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring.factories b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring.factories deleted file mode 100644 index dd6e28b8a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor:\ -org.springframework.cloud.stream.binder.kafka.common.KafkaBinderEnvironmentPostProcessor diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 0c5db2d81..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -org.springframework.cloud.stream.binder.kafka.config.ExtendedBindingHandlerMappingsProviderConfiguration diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AbstractKafkaTestBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AbstractKafkaTestBinder.java deleted file mode 100644 index 2035a5729..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AbstractKafkaTestBinder.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2014-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import org.springframework.cloud.stream.binder.AbstractPollableConsumerTestBinder; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.context.ApplicationContext; - -/** - * @author Soby Chacko - * @author Gary Russell - */ -public abstract class AbstractKafkaTestBinder extends - // @checkstyle:off - AbstractPollableConsumerTestBinder, ExtendedProducerProperties> { - - // @checkstyle:on - private ApplicationContext applicationContext; - - @Override - public void cleanup() { - // do nothing - the rule will take care of that - } - - protected final void setApplicationContext(ApplicationContext context) { - this.applicationContext = context; - } - - public ApplicationContext getApplicationContext() { - return this.applicationContext; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AdminConfigTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AdminConfigTests.java deleted file mode 100644 index 7f6b9f2d3..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AdminConfigTests.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka; - -import java.util.Arrays; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaTopicProperties; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.integration.config.EnableIntegration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Gary Russell - * @author Aldo Sinanaj - * @since 2.0 - * - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = { KafkaBinderConfiguration.class, - BindingServiceConfiguration.class }) -@TestPropertySource(properties = { - "spring.cloud.stream.kafka.bindings.input.consumer.topic.replication-factor=2", - "spring.cloud.stream.kafka.bindings.input.consumer.topic.replicas-assignments.0=0,1", - "spring.cloud.stream.kafka.bindings.input.consumer.topic.properties.message.format.version=0.9.0.0", - "spring.cloud.stream.kafka.bindings.secondInput.consumer.topic.replication-factor=3", - "spring.cloud.stream.kafka.bindings.secondInput.consumer.topic.replicas-assignments.0=0,1", - "spring.cloud.stream.kafka.bindings.secondInput.consumer.topic.properties.message.format.version=0.9.1.0", - "spring.cloud.stream.kafka.bindings.output.producer.topic.replication-factor=2", - "spring.cloud.stream.kafka.bindings.output.producer.topic.replicas-assignments.0=0,1", - "spring.cloud.stream.kafka.bindings.output.producer.topic.properties.message.format.version=0.9.0.0", - "spring.main.allow-bean-definition-overriding=true" }) -@EnableIntegration -public class AdminConfigTests { - - @Autowired - private KafkaMessageChannelBinder binder; - - @Test - void testConsumerTopicProperties() { - final KafkaConsumerProperties consumerProperties = this.binder - .getExtendedConsumerProperties("secondInput"); - final KafkaTopicProperties kafkaTopicProperties = consumerProperties.getTopic(); - - assertThat(kafkaTopicProperties.getReplicationFactor()).isEqualTo((short) 3); - assertThat(kafkaTopicProperties.getReplicasAssignments().get(0)) - .isEqualTo(Arrays.asList(0, 1)); - assertThat(kafkaTopicProperties.getProperties().get("message.format.version")) - .isEqualTo("0.9.1.0"); - } - - @Test - void testProducerTopicProperties() { - final KafkaProducerProperties producerProperties = this.binder - .getExtendedProducerProperties("output"); - final KafkaTopicProperties kafkaTopicProperties = producerProperties.getTopic(); - - assertThat(kafkaTopicProperties.getReplicationFactor()).isEqualTo((short) 2); - assertThat(kafkaTopicProperties.getReplicasAssignments().get(0)) - .isEqualTo(Arrays.asList(0, 1)); - assertThat(kafkaTopicProperties.getProperties().get("message.format.version")) - .isEqualTo("0.9.0.0"); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AutoCreateTopicDisabledTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AutoCreateTopicDisabledTests.java deleted file mode 100644 index b8006f745..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/AutoCreateTopicDisabledTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka; - -import java.util.Collections; - -import org.apache.kafka.common.errors.UnknownTopicOrPartitionException; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.BinderException; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.retry.policy.SimpleRetryPolicy; -import org.springframework.retry.support.RetryTemplate; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(brokerProperties = {"auto.create.topics.enable=false"}) -public class AutoCreateTopicDisabledTests { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @Test - void testAutoCreateTopicDisabledFailsOnConsumerIfTopicNonExistentOnBroker() { - - KafkaProperties kafkaProperties = new TestKafkaProperties(); - kafkaProperties.setBootstrapServers(Collections - .singletonList(embeddedKafka.getBrokersAsString())); - KafkaBinderConfigurationProperties configurationProperties = new KafkaBinderConfigurationProperties( - kafkaProperties); - // disable auto create topic on the binder. - configurationProperties.setAutoCreateTopics(false); - - KafkaTopicProvisioner provisioningProvider = new KafkaTopicProvisioner( - configurationProperties, kafkaProperties, prop -> { - }); - provisioningProvider.setMetadataRetryOperations(new RetryTemplate()); - - KafkaMessageChannelBinder binder = new KafkaMessageChannelBinder( - configurationProperties, provisioningProvider); - - final String testTopicName = "nonExistent" + System.currentTimeMillis(); - - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - new KafkaConsumerProperties()); - - assertThatExceptionOfType(BinderException.class) - .isThrownBy(() -> binder.createConsumerEndpoint(() -> testTopicName, "group", properties)) - .withCauseExactlyInstanceOf(UnknownTopicOrPartitionException.class); - } - - @Test - void testAutoCreateTopicDisabledFailsOnProducerIfTopicNonExistentOnBroker() { - - KafkaProperties kafkaProperties = new TestKafkaProperties(); - kafkaProperties.setBootstrapServers(Collections - .singletonList(embeddedKafka.getBrokersAsString())); - - KafkaBinderConfigurationProperties configurationProperties = new KafkaBinderConfigurationProperties( - kafkaProperties); - // disable auto create topic on the binder. - configurationProperties.setAutoCreateTopics(false); - // reduce the wait time on the producer blocking operations. - configurationProperties.getConfiguration().put("max.block.ms", "3000"); - - KafkaTopicProvisioner provisioningProvider = new KafkaTopicProvisioner( - configurationProperties, kafkaProperties, prop -> { - }); - SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(1); - final RetryTemplate metadataRetryOperations = new RetryTemplate(); - metadataRetryOperations.setRetryPolicy(simpleRetryPolicy); - provisioningProvider.setMetadataRetryOperations(metadataRetryOperations); - - KafkaMessageChannelBinder binder = new KafkaMessageChannelBinder( - configurationProperties, provisioningProvider); - - final String testTopicName = "nonExistent" + System.currentTimeMillis(); - - ExtendedProducerProperties properties = new ExtendedProducerProperties<>( - new KafkaProducerProperties()); - - assertThatExceptionOfType(BinderException.class) - .isThrownBy(() -> binder.bindProducer(testTopicName, new DirectChannel(), properties)) - .withCauseExactlyInstanceOf(UnknownTopicOrPartitionException.class); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderAutoConfigurationPropertiesTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderAutoConfigurationPropertiesTest.java deleted file mode 100644 index e5a9c12e1..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderAutoConfigurationPropertiesTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.kafka.common.serialization.LongDeserializer; -import org.apache.kafka.common.serialization.LongSerializer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Ilayaperumal Gopinathan - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = { KafkaBinderConfiguration.class }) -@TestPropertySource(locations = "classpath:binder-config-autoconfig.properties") -public class KafkaBinderAutoConfigurationPropertiesTest { - - @Autowired - private KafkaMessageChannelBinder kafkaMessageChannelBinder; - - @Autowired - private KafkaBinderHealthIndicator kafkaBinderHealthIndicator; - - @Test - @SuppressWarnings("unchecked") - void testKafkaBinderConfigurationWithKafkaProperties() throws Exception { - assertThat(this.kafkaMessageChannelBinder).isNotNull(); - ExtendedProducerProperties producerProperties = new ExtendedProducerProperties<>( - new KafkaProducerProperties()); - Method getProducerFactoryMethod = KafkaMessageChannelBinder.class - .getDeclaredMethod("getProducerFactory", String.class, - ExtendedProducerProperties.class, String.class, String.class); - getProducerFactoryMethod.setAccessible(true); - DefaultKafkaProducerFactory producerFactory = (DefaultKafkaProducerFactory) getProducerFactoryMethod - .invoke(this.kafkaMessageChannelBinder, "foo", producerProperties, "foo.producer", "foo"); - Field producerFactoryConfigField = ReflectionUtils - .findField(DefaultKafkaProducerFactory.class, "configs", Map.class); - ReflectionUtils.makeAccessible(producerFactoryConfigField); - Map producerConfigs = (Map) ReflectionUtils - .getField(producerFactoryConfigField, producerFactory); - assertThat(producerConfigs.get("batch.size").equals(10)).isTrue(); - assertThat(producerConfigs.get("key.serializer")).isEqualTo(LongSerializer.class); - assertThat(producerConfigs.get("key.deserializer")).isNull(); - assertThat(producerConfigs.get("value.serializer")) - .isEqualTo(LongSerializer.class); - assertThat(producerConfigs.get("value.deserializer")).isNull(); - assertThat(producerConfigs.get("compression.type")).isEqualTo("snappy"); - List bootstrapServers = new ArrayList<>(); - bootstrapServers.add("10.98.09.199:9092"); - bootstrapServers.add("10.98.09.196:9092"); - assertThat((((List) producerConfigs.get("bootstrap.servers")) - .containsAll(bootstrapServers))).isTrue(); - Method createKafkaConsumerFactoryMethod = KafkaMessageChannelBinder.class - .getDeclaredMethod("createKafkaConsumerFactory", boolean.class, - String.class, ExtendedConsumerProperties.class, String.class, String.class); - createKafkaConsumerFactoryMethod.setAccessible(true); - ExtendedConsumerProperties consumerProperties = new ExtendedConsumerProperties<>( - new KafkaConsumerProperties()); - DefaultKafkaConsumerFactory consumerFactory = (DefaultKafkaConsumerFactory) createKafkaConsumerFactoryMethod - .invoke(this.kafkaMessageChannelBinder, true, "test", consumerProperties, "test.consumer", "test"); - Field consumerFactoryConfigField = ReflectionUtils - .findField(DefaultKafkaConsumerFactory.class, "configs", Map.class); - ReflectionUtils.makeAccessible(consumerFactoryConfigField); - Map consumerConfigs = (Map) ReflectionUtils - .getField(consumerFactoryConfigField, consumerFactory); - assertThat(consumerConfigs.get("key.deserializer")) - .isEqualTo(LongDeserializer.class); - assertThat(consumerConfigs.get("key.serializer")).isNull(); - assertThat(consumerConfigs.get("value.deserializer")) - .isEqualTo(LongDeserializer.class); - assertThat(consumerConfigs.get("value.serialized")).isNull(); - assertThat(consumerConfigs.get("group.id")).isEqualTo("test"); - assertThat(consumerConfigs.get("auto.offset.reset")).isEqualTo("earliest"); - assertThat((((List) consumerConfigs.get("bootstrap.servers")) - .containsAll(bootstrapServers))).isTrue(); - } - - @Test - @SuppressWarnings("unchecked") - void testKafkaHealthIndicatorProperties() { - assertThat(this.kafkaBinderHealthIndicator).isNotNull(); - Field consumerFactoryField = ReflectionUtils.findField( - KafkaBinderHealthIndicator.class, "consumerFactory", - ConsumerFactory.class); - ReflectionUtils.makeAccessible(consumerFactoryField); - DefaultKafkaConsumerFactory consumerFactory = (DefaultKafkaConsumerFactory) ReflectionUtils - .getField(consumerFactoryField, this.kafkaBinderHealthIndicator); - Field configField = ReflectionUtils.findField(DefaultKafkaConsumerFactory.class, - "configs", Map.class); - ReflectionUtils.makeAccessible(configField); - Map configs = (Map) ReflectionUtils - .getField(configField, consumerFactory); - assertThat(configs.containsKey("bootstrap.servers")).isTrue(); - List bootstrapServers = new ArrayList<>(); - bootstrapServers.add("10.98.09.199:9092"); - bootstrapServers.add("10.98.09.196:9092"); - assertThat(((List) configs.get("bootstrap.servers")) - .containsAll(bootstrapServers)).isTrue(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderConfigurationPropertiesTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderConfigurationPropertiesTest.java deleted file mode 100644 index dbafc94fd..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderConfigurationPropertiesTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.apache.kafka.common.serialization.ByteArraySerializer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Ilayaperumal Gopinathan - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = { KafkaBinderConfiguration.class, KafkaAutoConfiguration.class, - KafkaBinderConfigurationPropertiesTest.class }) -@TestPropertySource(locations = "classpath:binder-config.properties") -public class KafkaBinderConfigurationPropertiesTest { - - @Autowired - private KafkaMessageChannelBinder kafkaMessageChannelBinder; - - @Test - @SuppressWarnings("unchecked") - void testKafkaBinderConfigurationProperties() throws Exception { - assertThat(this.kafkaMessageChannelBinder).isNotNull(); - KafkaProducerProperties kafkaProducerProperties = new KafkaProducerProperties(); - kafkaProducerProperties.setBufferSize(12345); - kafkaProducerProperties.setBatchTimeout(100); - kafkaProducerProperties.setCloseTimeout(10); - kafkaProducerProperties - .setCompressionType(KafkaProducerProperties.CompressionType.gzip); - ExtendedProducerProperties producerProperties = new ExtendedProducerProperties<>( - kafkaProducerProperties); - Method getProducerFactoryMethod = KafkaMessageChannelBinder.class - .getDeclaredMethod("getProducerFactory", String.class, - ExtendedProducerProperties.class, String.class, String.class); - getProducerFactoryMethod.setAccessible(true); - DefaultKafkaProducerFactory producerFactory = (DefaultKafkaProducerFactory) getProducerFactoryMethod - .invoke(this.kafkaMessageChannelBinder, "bar", producerProperties, "bar.producer", "bar"); - Field producerFactoryConfigField = ReflectionUtils - .findField(DefaultKafkaProducerFactory.class, "configs", Map.class); - ReflectionUtils.makeAccessible(producerFactoryConfigField); - Map producerConfigs = (Map) ReflectionUtils - .getField(producerFactoryConfigField, producerFactory); - assertThat(producerConfigs.get("batch.size")).isEqualTo("12345"); - assertThat(producerConfigs.get("linger.ms")).isEqualTo("100"); - assertThat(producerConfigs.get("key.serializer")) - .isEqualTo(ByteArraySerializer.class); - assertThat(producerConfigs.get("value.serializer")) - .isEqualTo(ByteArraySerializer.class); - assertThat(producerConfigs.get("compression.type")).isEqualTo("gzip"); - - Field physicalCloseTimeoutField = ReflectionUtils - .findField(DefaultKafkaProducerFactory.class, "physicalCloseTimeout", Duration.class); - ReflectionUtils.makeAccessible(physicalCloseTimeoutField); - Duration physicalCloseTimeoutConfig = (Duration) ReflectionUtils - .getField(physicalCloseTimeoutField, producerFactory); - assertThat(physicalCloseTimeoutConfig).isEqualTo(Duration.ofSeconds(10)); - - List bootstrapServers = new ArrayList<>(); - bootstrapServers.add("10.98.09.199:9082"); - assertThat((((String) producerConfigs.get("bootstrap.servers")) - .contains("10.98.09.199:9082"))).isTrue(); - Method createKafkaConsumerFactoryMethod = KafkaMessageChannelBinder.class - .getDeclaredMethod("createKafkaConsumerFactory", boolean.class, - String.class, ExtendedConsumerProperties.class, String.class, String.class); - createKafkaConsumerFactoryMethod.setAccessible(true); - ExtendedConsumerProperties consumerProperties = new ExtendedConsumerProperties<>( - new KafkaConsumerProperties()); - DefaultKafkaConsumerFactory consumerFactory = (DefaultKafkaConsumerFactory) createKafkaConsumerFactoryMethod - .invoke(this.kafkaMessageChannelBinder, true, "test", consumerProperties, "test.consumer", "test"); - Field consumerFactoryConfigField = ReflectionUtils - .findField(DefaultKafkaConsumerFactory.class, "configs", Map.class); - ReflectionUtils.makeAccessible(consumerFactoryConfigField); - Map consumerConfigs = (Map) ReflectionUtils - .getField(consumerFactoryConfigField, consumerFactory); - assertThat(consumerConfigs.get("key.deserializer")) - .isEqualTo(ByteArrayDeserializer.class); - assertThat(consumerConfigs.get("value.deserializer")) - .isEqualTo(ByteArrayDeserializer.class); - assertThat((((String) consumerConfigs.get("bootstrap.servers")) - .contains("10.98.09.199:9082"))).isTrue(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderConfigurationTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderConfigurationTest.java deleted file mode 100644 index 05c455df0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderConfigurationTest.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.stream.binder.kafka.config.ClientFactoryCustomizer; -import org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration; -import org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.util.ReflectionUtils; - -import static org.apache.kafka.clients.admin.AdminClientConfig.SECURITY_PROTOCOL_CONFIG; -import static org.apache.kafka.common.config.SaslConfigs.SASL_MECHANISM; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.util.ReflectionUtils.getField; - -/** - * @author Ilayaperumal Gopinathan - * @author Chris Bono - */ -@SpringBootTest(classes = { KafkaBinderConfiguration.class }) -class KafkaBinderConfigurationTest { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(KafkaBinderConfiguration.class, KafkaAutoConfiguration.class); - - @Test - void binderAutoConfiguredWithProducerListener(@Autowired KafkaMessageChannelBinder kafkaMessageChannelBinder) { - assertThat(ReflectionTestUtils.getField(kafkaMessageChannelBinder, "producerListener")).isNotNull(); - } - - @Test - public void testMultipleClientFactoryCustomizers() { - contextRunner.withUserConfiguration(ClientFactoryCustomizerConfigs.class) - .run(context -> { - assertThat(context).hasSingleBean(KafkaMessageChannelBinder.class); - KafkaMessageChannelBinder kafkaMessageChannelBinder = - context.getBean(KafkaMessageChannelBinder.class); - Map customizers = - context.getBeansOfType(ClientFactoryCustomizer.class); - assertThat(customizers).hasSize(2); - Field clientFactoryCustomizersField = ReflectionUtils.findField( - KafkaMessageChannelBinder.class, "clientFactoryCustomizers", - List.class); - ReflectionUtils.makeAccessible(clientFactoryCustomizersField); - List clientFactoryCustomizers = - (List) getField(clientFactoryCustomizersField, kafkaMessageChannelBinder); - assertThat(clientFactoryCustomizers).hasSize(2); - }); - } - - @Test - public void testMultipleAdminClientConfigCustomizers() { - contextRunner.withUserConfiguration(AdminClientConfigCustomizerConfigs.class) - .run(context -> { - assertThat(context).hasSingleBean(KafkaTopicProvisioner.class); - KafkaTopicProvisioner kafkaTopicProvisioner = context.getBean(KafkaTopicProvisioner.class); - Map customizers = - context.getBeansOfType(AdminClientConfigCustomizer.class); - assertThat(customizers).hasSize(2); - Field adminClientPropertiesField = ReflectionUtils.findField( - KafkaTopicProvisioner.class, "adminClientProperties", Map.class); - ReflectionUtils.makeAccessible(adminClientPropertiesField); - Map adminClientProperties = (Map) ReflectionUtils. - getField(adminClientPropertiesField, kafkaTopicProvisioner); - assertThat(adminClientProperties.get(SECURITY_PROTOCOL_CONFIG)).isEqualTo("SASL_SSL"); - assertThat(adminClientProperties.get(SASL_MECHANISM)).isEqualTo("OAUTHBEARER"); - }); - } - - @Configuration - static class ClientFactoryCustomizerConfigs { - - @Bean - ClientFactoryCustomizer testClientFactoryCustomizerOne() { - return new ClientFactoryCustomizer() { - @Override - public void configure(ProducerFactory pf) { - ClientFactoryCustomizer.super.configure(pf); - } - }; - } - - @Bean - ClientFactoryCustomizer testClientFactoryCustomizerTwo() { - return new ClientFactoryCustomizer() { - @Override - public void configure(ProducerFactory pf) { - ClientFactoryCustomizer.super.configure(pf); - } - }; - } - } - - @Configuration - static class AdminClientConfigCustomizerConfigs { - - @Bean - AdminClientConfigCustomizer testAdminClientConfigCustomizerOne() { - return prop -> prop.put(SECURITY_PROTOCOL_CONFIG, "SASL_SSL"); - } - - @Bean - AdminClientConfigCustomizer testAdminClientConfigCustomizerTwo() { - return prop -> prop.put(SASL_MECHANISM, "OAUTHBEARER"); - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealthIndicatorTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealthIndicatorTest.java deleted file mode 100644 index ba22ace6a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderHealthIndicatorTest.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.common.KafkaException; -import org.apache.kafka.common.Node; -import org.apache.kafka.common.PartitionInfo; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.Timeout; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; - -import static java.util.Collections.singleton; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; - -/** - * @author Barry Commins - * @author Gary Russell - * @author Laur Aliste - * @author Soby Chacko - * @author Chukwubuikem Ume-Ugwa - * @author Taras Danylchuk - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class KafkaBinderHealthIndicatorTest { - - private static final String TEST_TOPIC = "test"; - - private static final String REGEX_TOPIC = "regex*"; - - private KafkaBinderHealthIndicator indicator; - - @Mock - private DefaultKafkaConsumerFactory consumerFactory; - - @Mock - private KafkaConsumer consumer; - - @Mock - AbstractMessageListenerContainer listenerContainerA; - - @Mock - AbstractMessageListenerContainer listenerContainerB; - - @Mock - private KafkaMessageChannelBinder binder; - - private final Map topicsInUse = new HashMap<>(); - - @BeforeEach - public void setup() { - MockitoAnnotations.initMocks(this); - org.mockito.BDDMockito.given(consumerFactory.createConsumer()) - .willReturn(consumer); - org.mockito.BDDMockito.given(binder.getTopicsInUse()).willReturn(topicsInUse); - this.indicator = new KafkaBinderHealthIndicator(binder, consumerFactory); - this.indicator.setTimeout(10); - } - - @Test - void kafkaBinderIsUpWithNoConsumers() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group1-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - org.mockito.BDDMockito.given(binder.getKafkaMessageListenerContainers()) - .willReturn(Collections.emptyList()); - - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("topicsInUse", singleton(TEST_TOPIC)); - } - - @Test - void kafkaBinderIsUp() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group1-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - org.mockito.BDDMockito.given(binder.getKafkaMessageListenerContainers()) - .willReturn(Arrays.asList(listenerContainerA, listenerContainerB)); - mockContainer(listenerContainerA, true, true); - mockContainer(listenerContainerB, true, true); - - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("topicsInUse", singleton(TEST_TOPIC)); - assertThat(health.getDetails()).hasEntrySatisfying("listenerContainers", value -> - assertThat((ArrayList) value).hasSize(2)); - } - - @Test - void kafkaBinderIsDownWhenOneOfConsumersIsNotRunning() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group1-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - org.mockito.BDDMockito.given(binder.getKafkaMessageListenerContainers()) - .willReturn(Arrays.asList(listenerContainerA, listenerContainerB)); - mockContainer(listenerContainerA, false, true); - mockContainer(listenerContainerB, true, true); - - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsEntry("topicsInUse", singleton(TEST_TOPIC)); - assertThat(health.getDetails()).hasEntrySatisfying("listenerContainers", value -> - assertThat((ArrayList) value).hasSize(2)); - } - - @Test - void kafkaBinderIsDownWhenOneOfContainersWasStoppedAbnormally() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group1-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - org.mockito.BDDMockito.given(binder.getKafkaMessageListenerContainers()) - .willReturn(Arrays.asList(listenerContainerA, listenerContainerB)); - mockContainer(listenerContainerA, false, false); - mockContainer(listenerContainerB, true, true); - - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - assertThat(health.getDetails()).containsEntry("topicsInUse", singleton(TEST_TOPIC)); - assertThat(health.getDetails()).hasEntrySatisfying("listenerContainers", value -> - assertThat((ArrayList) value).hasSize(2)); - } - - private void mockContainer(AbstractMessageListenerContainer container, boolean isRunning, - boolean normalState) { - - org.mockito.BDDMockito.given(container.isRunning()).willReturn(isRunning); - org.mockito.BDDMockito.given(container.isContainerPaused()).willReturn(true); - org.mockito.BDDMockito.given(container.getListenerId()).willReturn("someListenerId"); - org.mockito.BDDMockito.given(container.getGroupId()).willReturn("someGroupId"); - org.mockito.BDDMockito.given(container.isInExpectedState()).willReturn(normalState); - } - - @Test - void kafkaBinderIsUpWithRegexTopic() { - topicsInUse.put(REGEX_TOPIC, new TopicInformation( - "regex-healthIndicator", null, true)); - Health health = indicator.health(); - // verify no consumer interaction for retrieving partitions - org.mockito.BDDMockito.verify(consumer, Mockito.never()) - .partitionsFor(REGEX_TOPIC); - // Ensuring the normal health check returns with status "up" - assertThat(health.getStatus()).isEqualTo(Status.UP); - } - - @Test - void downWhenListTopicsThrowExceptionWithRegexTopic() { - topicsInUse.put(REGEX_TOPIC, new TopicInformation( - "regex-healthIndicator", null, true)); - org.mockito.BDDMockito.given(consumer.listTopics(any(Duration.class))) - .willThrow(new IllegalStateException()); - - Health health = indicator.health(); - - // Ensuring the normal health check returns with status "up" - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - Map details = health.getDetails(); - assertThat(details.containsKey("Cluster not connected")).isTrue(); - assertThat(details - .containsValue("Destination provided is a pattern, but cannot connect to the cluster for any verification")).isTrue(); - } - - - @Test - void kafkaBinderIsDown() { - final List partitions = partitions(null); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group2-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - } - - @Test - void kafkaBinderIsDownWhenConsiderDownWhenAnyPartitionHasNoLeaderIsTrue() { - final List partitions = partitions(new Node(0, null, 0)); - partitions.add(new PartitionInfo(TEST_TOPIC, 0, null, null, null)); - indicator.setConsiderDownWhenAnyPartitionHasNoLeader(true); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group2-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - } - - @Test - void kafkaBinderIsUpWhenConsiderDownWhenAnyPartitionHasNoLeaderIsFalse() { - Node node = new Node(0, null, 0); - final List partitions = partitions(node); - partitions.add(new PartitionInfo(TEST_TOPIC, 0, null, null, null)); - indicator.setConsiderDownWhenAnyPartitionHasNoLeader(false); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group2-healthIndicator", partitions(node), false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - } - - @Test - @Timeout(5) - void kafkaBinderDoesNotAnswer() { - final List partitions = partitions(new Node(-1, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group3-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willAnswer(invocation -> { - final int fiveMinutes = 1000 * 60 * 5; - Thread.sleep(fiveMinutes); - return partitions; - }); - this.indicator.setTimeout(1); - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - } - - @Test - void createsConsumerOnceWhenInvokedMultipleTimes() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "group4-healthIndicator", partitions, false)); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - - indicator.health(); - Health health = indicator.health(); - - assertThat(health.getStatus()).isEqualTo(Status.UP); - org.mockito.Mockito.verify(this.consumerFactory).createConsumer(); - } - - @Test - void consumerCreationFailsFirstTime() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation( - "foo-healthIndicator", partitions, false)); - - org.mockito.BDDMockito.given(consumerFactory.createConsumer()) - .willThrow(KafkaException.class).willReturn(consumer); - - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - - health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - - org.mockito.Mockito.verify(this.consumerFactory, Mockito.times(2)) - .createConsumer(); - } - - @Test - void testIfNoTopicsRegisteredByTheBinderProvidesDownStatus() { - Health health = indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UNKNOWN); - } - - private List partitions(Node leader) { - List partitions = new ArrayList<>(); - partitions.add(new PartitionInfo(TEST_TOPIC, 0, leader, null, null)); - return partitions; - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderJaasInitializerListenerTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderJaasInitializerListenerTest.java deleted file mode 100644 index e5522b229..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderJaasInitializerListenerTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka; - -import javax.security.auth.login.AppConfigurationEntry; -import javax.security.auth.login.Configuration; - -import com.sun.security.auth.login.ConfigFile; -import org.apache.kafka.common.security.JaasUtils; -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.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Marius Bogoevici - * @author Soby Chacko - */ -@EmbeddedKafka -public class KafkaBinderJaasInitializerListenerTest { - - private static final String KAFKA_BROKERS_PROPERTY = "spring.cloud.stream.kafka.binder.brokers"; - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - private static String JAVA_LOGIN_CONFIG_PARAM_VALUE; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(KafkaBinderConfiguration.class, KafkaAutoConfiguration.class); - - @BeforeAll - public static void setup() { - System.setProperty(KAFKA_BROKERS_PROPERTY, - embeddedKafka.getBrokersAsString()); - //Retrieve the current value for this system property if there is one set. - JAVA_LOGIN_CONFIG_PARAM_VALUE = System.getProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - System.clearProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - } - - @AfterAll - public static void clean() { - System.clearProperty(KAFKA_BROKERS_PROPERTY); - //If there was a previous value for this property, then restore it. - if (JAVA_LOGIN_CONFIG_PARAM_VALUE != null) { - System.setProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM, JAVA_LOGIN_CONFIG_PARAM_VALUE); - } - } - - @BeforeEach - public void before() { - System.clearProperty(JaasUtils.JAVA_LOGIN_CONFIG_PARAM); - // Invalidate any existing security configuration so that tests are forced to create/use fresh configuration. - Configuration.setConfiguration(null); - } - - @Test - void testConfigurationParsedCorrectlyWithKafkaClientAndDefaultControlFlag() - throws Exception { - ConfigFile configFile = new ConfigFile( - new ClassPathResource("jaas-sample-kafka-only.conf").getURI()); - final AppConfigurationEntry[] kafkaConfigurationArray = configFile - .getAppConfigurationEntry("KafkaClient"); - - - this.contextRunner - .withPropertyValues("spring.cloud.stream.kafka.binder.jaas.options.useKeyTab=true", - "spring.cloud.stream.kafka.binder.jaas.options.storeKey=true", - "spring.cloud.stream.kafka.binder.jaas.options.keyTab=/etc/security/keytabs/kafka_client.keytab", - "spring.cloud.stream.kafka.binder.jaas.options.principal=kafka-client-1@EXAMPLE.COM", - "spring.jmx.enabled=false") - .run(context -> { - javax.security.auth.login.Configuration configuration = javax.security.auth.login.Configuration - .getConfiguration(); - - final AppConfigurationEntry[] kafkaConfiguration = configuration - .getAppConfigurationEntry("KafkaClient"); - assertThat(kafkaConfiguration).hasSize(1); - assertThat(kafkaConfiguration[0].getOptions()) - .isEqualTo(kafkaConfigurationArray[0].getOptions()); - assertThat(kafkaConfiguration[0].getControlFlag()) - .isEqualTo(AppConfigurationEntry.LoginModuleControlFlag.REQUIRED); - }); - } - - @Test - void testConfigurationParsedCorrectlyWithKafkaClientAndNonDefaultControlFlag() - throws Exception { - ConfigFile configFile = new ConfigFile( - new ClassPathResource("jaas-sample-kafka-only.conf").getURI()); - final AppConfigurationEntry[] kafkaConfigurationArray = configFile - .getAppConfigurationEntry("KafkaClient"); - - this.contextRunner - .withPropertyValues("spring.cloud.stream.kafka.binder.jaas.options.useKeyTab=true", - "spring.cloud.stream.kafka.binder.jaas.controlFlag=requisite", - "spring.cloud.stream.kafka.binder.jaas.options.storeKey=true", - "spring.cloud.stream.kafka.binder.jaas.options.keyTab=/etc/security/keytabs/kafka_client.keytab", - "spring.cloud.stream.kafka.binder.jaas.options.principal=kafka-client-1@EXAMPLE.COM", - "spring.jmx.enabled=false") - .run(context -> { - javax.security.auth.login.Configuration configuration = javax.security.auth.login.Configuration - .getConfiguration(); - - final AppConfigurationEntry[] kafkaConfiguration = configuration - .getAppConfigurationEntry("KafkaClient"); - assertThat(kafkaConfiguration).hasSize(1); - assertThat(kafkaConfiguration[0].getOptions()) - .isEqualTo(kafkaConfigurationArray[0].getOptions()); - assertThat(kafkaConfiguration[0].getControlFlag()) - .isEqualTo(AppConfigurationEntry.LoginModuleControlFlag.REQUISITE); - context.close(); - }); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderMetricsTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderMetricsTest.java deleted file mode 100644 index 5bcaf02f1..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderMetricsTest.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder.kafka; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.clients.consumer.OffsetAndMetadata; -import org.apache.kafka.common.KafkaException; -import org.apache.kafka.common.Node; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.TopicPartition; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Answers; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * @author Henryk Konsek - * @author Thomas Cheyney - * @author Soby Chacko - * @author Lars Bilger - * @author Tomek Szmytka - * @author Nico Heller - */ -public class KafkaBinderMetricsTest { - - private static final String TEST_TOPIC = "test"; - - private KafkaBinderMetrics metrics; - - @Mock - private DefaultKafkaConsumerFactory consumerFactory; - - @Mock - private KafkaConsumer consumer; - - @Mock - private KafkaMessageChannelBinder binder; - - private MeterRegistry meterRegistry = new SimpleMeterRegistry(); - - private Map topicsInUse = new HashMap<>(); - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - org.mockito.BDDMockito.given(consumerFactory - .createConsumer(ArgumentMatchers.any(), ArgumentMatchers.any())) - .willReturn(consumer); - org.mockito.BDDMockito.given(binder.getTopicsInUse()).willReturn(topicsInUse); - org.mockito.BDDMockito.given(kafkaBinderConfigurationProperties.getMetrics().isDefaultOffsetLagMetricsEnabled()) - .willReturn(true); - org.mockito.BDDMockito.given(kafkaBinderConfigurationProperties.getMetrics().getOffsetLagMetricsInterval()) - .willReturn(Duration.ofSeconds(60)); - metrics = new KafkaBinderMetrics(binder, kafkaBinderConfigurationProperties, - consumerFactory, null - ); - org.mockito.BDDMockito - .given(consumer.endOffsets(ArgumentMatchers.anyCollection())) - .willReturn(java.util.Collections - .singletonMap(new TopicPartition(TEST_TOPIC, 0), 1000L)); - } - - @Test - void shouldIndicateLag() { - final Map committed = new HashMap<>(); - TopicPartition topicPartition = new TopicPartition(TEST_TOPIC, 0); - committed.put(topicPartition, new OffsetAndMetadata(500)); - org.mockito.BDDMockito - .given(consumer.committed(ArgumentMatchers.anySet())) - .willReturn(committed); - List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group1-metrics", partitions, false) - ); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - metrics.bindTo(meterRegistry); - assertThat(meterRegistry.getMeters()).hasSize(1); - assertThat(meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group1-metrics").tag("topic", TEST_TOPIC).gauge().value()) - .isEqualTo(500.0); - } - - @Test - void shouldFallbackToScheduledOffsetLagComputationWhenRealtimeOffsetLagIsDisabled() { - final Map committed = new HashMap<>(); - TopicPartition topicPartition = new TopicPartition(TEST_TOPIC, 0); - committed.put(topicPartition, new OffsetAndMetadata(500)); - org.mockito.BDDMockito - .given(consumer.committed(ArgumentMatchers.anySet())) - .willReturn(committed); - List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group1-metrics", partitions, false) - ); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - org.mockito.BDDMockito.given(kafkaBinderConfigurationProperties.getMetrics().isDefaultOffsetLagMetricsEnabled()) - .willReturn(false); - metrics.bindTo(meterRegistry); - assertThat(meterRegistry.getMeters()).hasSize(1); - assertThat(meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group1-metrics").tag("topic", TEST_TOPIC).gauge().value()) - .isEqualTo(0); - - org.mockito.BDDMockito.given(kafkaBinderConfigurationProperties.getMetrics().getOffsetLagMetricsInterval()) - .willReturn(Duration.ofSeconds(1)); - metrics.bindTo(meterRegistry); - Awaitility.waitAtMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group1-metrics").tag("topic", TEST_TOPIC).gauge().value()) - .isEqualTo(500); - }); - } - - @Test - void shouldNotContainAnyMetricsWhenUsingNoopGauge() { - // Adding NoopGauge for the offset metric. - meterRegistry.config().meterFilter( - MeterFilter.denyNameStartsWith("spring.cloud.stream.binder.kafka.offset")); - - // Because we have NoopGauge for the offset metric in the meter registry, none of these expectations matter. - org.mockito.BDDMockito - .given(consumer.committed(ArgumentMatchers.any(TopicPartition.class))) - .willReturn(new OffsetAndMetadata(500)); - List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group1-metrics", partitions, false) - ); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - metrics.bindTo(meterRegistry); - - // Because of the NoopGauge, the meterRegistry should contain no metric. - assertThat(meterRegistry.getMeters()).hasSize(0); - } - - @Test - void shouldSumUpPartitionsLags() { - Map endOffsets = new HashMap<>(); - endOffsets.put(new TopicPartition(TEST_TOPIC, 0), 1000L); - endOffsets.put(new TopicPartition(TEST_TOPIC, 1), 1000L); - org.mockito.BDDMockito - .given(consumer.endOffsets(ArgumentMatchers.anyCollection())) - .willReturn(endOffsets); - final Map committed = new HashMap<>(); - TopicPartition topicPartition1 = new TopicPartition(TEST_TOPIC, 0); - TopicPartition topicPartition2 = new TopicPartition(TEST_TOPIC, 1); - committed.put(topicPartition1, new OffsetAndMetadata(500)); - committed.put(topicPartition2, new OffsetAndMetadata(500)); - org.mockito.BDDMockito - .given(consumer.committed(ArgumentMatchers.anySet())) - .willReturn(committed); - List partitions = partitions( - new Node(0, null, 0), - new Node(0, null, 0) - ); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group2-metrics", partitions, false) - ); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - metrics.bindTo(meterRegistry); - assertThat(meterRegistry.getMeters()).hasSize(1); - assertThat(meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group2-metrics").tag("topic", TEST_TOPIC).gauge().value()) - .isEqualTo(1000.0); - } - - @Test - void shouldIndicateFullLagForNotCommittedGroups() { - List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group3-metrics", partitions, false) - ); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - metrics.bindTo(meterRegistry); - assertThat(meterRegistry.getMeters()).hasSize(1); - assertThat(meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group3-metrics").tag("topic", TEST_TOPIC).gauge().value()) - .isEqualTo(1000.0); - } - - @Test - void shouldNotCalculateLagForProducerTopics() { - List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put(TEST_TOPIC, new TopicInformation(null, partitions, false)); - metrics.bindTo(meterRegistry); - assertThat(meterRegistry.getMeters()).isEmpty(); - } - - @Test - void createsConsumerOnceWhenInvokedMultipleTimes() { - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group4-metrics", partitions, false) - ); - - metrics.bindTo(meterRegistry); - - Gauge gauge = meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group4-metrics").tag("topic", TEST_TOPIC).gauge(); - gauge.value(); - assertThat(gauge.value()).isEqualTo(1000.0); - - org.mockito.Mockito.verify(this.consumerFactory) - .createConsumer(ArgumentMatchers.any(), ArgumentMatchers.any()); - } - - @Test - void consumerCreationFailsFirstTime() { - org.mockito.BDDMockito - .given(consumerFactory.createConsumer( - ArgumentMatchers.any(), - ArgumentMatchers.any() - )) - .willThrow(KafkaException.class).willReturn(consumer); - - final List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group5-metrics", partitions, false) - ); - - metrics.bindTo(meterRegistry); - - Gauge gauge = meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group5-metrics").tag("topic", TEST_TOPIC).gauge(); - assertThat(gauge.value()).isEqualTo(0); - assertThat(gauge.value()).isEqualTo(1000.0); - - org.mockito.Mockito.verify(this.consumerFactory, Mockito.times(2)) - .createConsumer(ArgumentMatchers.any(), ArgumentMatchers.any()); - } - - @Test - void createOneConsumerPerGroup() { - final List partitions1 = partitions(new Node(0, null, 0)); - final List partitions2 = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group1-metrics", partitions1, false) - ); - topicsInUse.put( - "test2", - new TopicInformation("group2-metrics", partitions2, false) - ); - - metrics.bindTo(meterRegistry); - - KafkaConsumer consumer2 = mock(KafkaConsumer.class); - org.mockito.BDDMockito - .given(consumerFactory.createConsumer( - ArgumentMatchers.eq("group2-metrics"), ArgumentMatchers.any())) - .willReturn(consumer2); - org.mockito.BDDMockito - .given(consumer2.endOffsets(ArgumentMatchers.anyCollection())) - .willReturn(java.util.Collections - .singletonMap(new TopicPartition("test2", 0), 50L)); - - Gauge gauge1 = meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group1-metrics").tag("topic", TEST_TOPIC).gauge(); - Gauge gauge2 = meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group2-metrics").tag("topic", "test2").gauge(); - gauge1.value(); - gauge2.value(); - assertThat(gauge1.value()).isEqualTo(1000.0); - assertThat(gauge2.value()).isEqualTo(50.0); - - org.mockito.Mockito.verify(this.consumerFactory).createConsumer( - ArgumentMatchers.eq("group1-metrics"), ArgumentMatchers.any()); - org.mockito.Mockito.verify(this.consumerFactory).createConsumer( - ArgumentMatchers.eq("group2-metrics"), ArgumentMatchers.any()); - } - - @Test - public void usesBeginningOffsetIfNoCommittedOffsetFound() { - org.mockito.BDDMockito - .given(consumer.committed(ArgumentMatchers.anySet())) - .willReturn(Collections.emptyMap()); - final Map beginnings = new HashMap<>(); - TopicPartition topicPartition = new TopicPartition(TEST_TOPIC, 0); - beginnings.put(topicPartition, 500L); - org.mockito.BDDMockito - .given(consumer.beginningOffsets(ArgumentMatchers.anySet())) - .willReturn(beginnings); - List partitions = partitions(new Node(0, null, 0)); - topicsInUse.put( - TEST_TOPIC, - new TopicInformation("group1-metrics", partitions, false) - ); - org.mockito.BDDMockito.given(consumer.partitionsFor(TEST_TOPIC)) - .willReturn(partitions); - metrics.bindTo(meterRegistry); - assertThat(meterRegistry.getMeters()).hasSize(1); - assertThat(meterRegistry.get(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME) - .tag("group", "group1-metrics").tag("topic", TEST_TOPIC).gauge().value()) - .isEqualTo(500.0); - } - - @Test - public void shouldShutdownSchedulerOnClose() throws Exception { - metrics.bindTo(meterRegistry); - metrics.close(); - assertThat(metrics.scheduler.isShutdown()).isTrue(); - } - - private List partitions(Node... nodes) { - List partitions = new ArrayList<>(); - for (int i = 0; i < nodes.length; i++) { - partitions.add(new PartitionInfo(TEST_TOPIC, i, nodes[i], null, null)); - } - return partitions; - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java deleted file mode 100644 index bb44daa9f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java +++ /dev/null @@ -1,4172 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -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.stream.IntStream; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.tck.TestObservationRegistry; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.CreateTopicsResult; -import org.apache.kafka.clients.admin.DescribeTopicsResult; -import org.apache.kafka.clients.admin.NewPartitions; -import org.apache.kafka.clients.admin.NewTopic; -import org.apache.kafka.clients.admin.TopicDescription; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.clients.producer.RecordMetadata; -import org.apache.kafka.common.KafkaFuture; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.errors.TopicExistsException; -import org.apache.kafka.common.header.Header; -import org.apache.kafka.common.header.Headers; -import org.apache.kafka.common.header.internals.RecordHeader; -import org.apache.kafka.common.record.TimestampType; -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.apache.kafka.common.serialization.ByteArraySerializer; -import org.apache.kafka.common.serialization.Deserializer; -import org.apache.kafka.common.serialization.LongDeserializer; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.Condition; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.mockito.ArgumentMatchers; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderException; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.HeaderMode; -import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.binder.PartitionTestSupport; -import org.springframework.cloud.stream.binder.PollableSource; -import org.springframework.cloud.stream.binder.RequeueCurrentMessageException; -import org.springframework.cloud.stream.binder.Spy; -import org.springframework.cloud.stream.binder.TestUtils; -import org.springframework.cloud.stream.binder.kafka.common.TopicInformation; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.utils.DlqDestinationResolver; -import org.springframework.cloud.stream.binder.kafka.utils.DlqPartitionFunction; -import org.springframework.cloud.stream.binder.kafka.utils.KafkaTopicUtils; -import org.springframework.cloud.stream.binding.MessageConverterConfigurer.PartitioningInterceptor; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.cloud.stream.provisioning.ProvisioningException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.common.LiteralExpression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.handler.BridgeHandler; -import org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter; -import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; -import org.springframework.integration.kafka.support.KafkaSendFailureException; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaAdmin; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.kafka.listener.CommonErrorHandler; -import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; -import org.springframework.kafka.listener.ContainerProperties; -import org.springframework.kafka.listener.DefaultErrorHandler; -import org.springframework.kafka.listener.MessageListenerContainer; -import org.springframework.kafka.support.Acknowledgment; -import org.springframework.kafka.support.KafkaHeaderMapper; -import org.springframework.kafka.support.KafkaHeaders; -import org.springframework.kafka.support.SendResult; -import org.springframework.kafka.support.TopicPartitionOffset; -import org.springframework.kafka.support.converter.BatchMessagingMessageConverter; -import org.springframework.kafka.support.converter.MessagingMessageConverter; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.core.BrokerAddress; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHandlingException; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.Assert; -import org.springframework.util.MimeTypeUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.backoff.FixedBackOff; - - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.entry; -import static org.assertj.core.api.Assertions.fail; -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 Soby Chacko - * @author Ilayaperumal Gopinathan - * @author Henryk Konsek - * @author Gary Russell - * @author Chris Bono - * @Author Oliver FΓΌhrer - */ -@EmbeddedKafka(count = 1, controlledShutdown = true, topics = "error.pollableDlq.group-pcWithDlq", brokerProperties = {"transaction.state.log.replication.factor=1", - "transaction.state.log.min.isr=1"}) -public class KafkaBinderTests extends - - PartitionCapableBinderTests, ExtendedProducerProperties> { - - private static final int DEFAULT_OPERATION_TIMEOUT = 30; - - private final String CLASS_UNDER_TEST_NAME = KafkaMessageChannelBinder.class - .getSimpleName(); - - private KafkaTestBinder binder; - - private AdminClient adminClient; - - private static EmbeddedKafkaBroker embeddedKafka; - - @BeforeAll - public static void setup() { - embeddedKafka = EmbeddedKafkaCondition.getBroker(); - } - - @Override - protected ExtendedConsumerProperties createConsumerProperties() { - final ExtendedConsumerProperties kafkaConsumerProperties = new ExtendedConsumerProperties<>( - new KafkaConsumerProperties()); - // set the default values that would normally be propagated by Spring Cloud Stream - kafkaConsumerProperties.setInstanceCount(1); - kafkaConsumerProperties.setInstanceIndex(0); - return kafkaConsumerProperties; - } - - private ExtendedProducerProperties createProducerProperties() { - return this.createProducerProperties(null); - } - - @Override - protected ExtendedProducerProperties createProducerProperties(TestInfo testInto) { - ExtendedProducerProperties producerProperties = new ExtendedProducerProperties<>( - new KafkaProducerProperties()); - producerProperties.getExtension().setSync(true); - return producerProperties; - } - - @Override - protected void binderBindUnbindLatency() throws InterruptedException { - Thread.sleep(500); - } - - @Override - protected KafkaTestBinder getBinder() { - if (binder == null) { - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - KafkaTopicProvisioner kafkaTopicProvisioner = new KafkaTopicProvisioner( - binderConfiguration, new TestKafkaProperties(), prop -> { - }); - try { - kafkaTopicProvisioner.afterPropertiesSet(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - binder = new KafkaTestBinder(binderConfiguration, kafkaTopicProvisioner); - } - return binder; - } - - private KafkaTestBinder getBinder( - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties) { - - return getBinder(kafkaBinderConfigurationProperties, null, null); - } - - private KafkaTestBinder getBinder( - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties, - DlqPartitionFunction dlqPartitionFunction, DlqDestinationResolver dlqDestinationResolver) { - - KafkaTopicProvisioner provisioningProvider = new KafkaTopicProvisioner( - kafkaBinderConfigurationProperties, new TestKafkaProperties(), prop -> { - }); - try { - provisioningProvider.afterPropertiesSet(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - return new KafkaTestBinder(kafkaBinderConfigurationProperties, - provisioningProvider, dlqPartitionFunction, dlqDestinationResolver); - } - - private KafkaBinderConfigurationProperties createConfigurationProperties() { - KafkaBinderConfigurationProperties binderConfiguration = new KafkaBinderConfigurationProperties( - new TestKafkaProperties()); - BrokerAddress[] brokerAddresses = embeddedKafka.getBrokerAddresses(); - - List bAddresses = new ArrayList<>(); - for (BrokerAddress bAddress : brokerAddresses) { - bAddresses.add(bAddress.toString()); - } - String[] foo = new String[bAddresses.size()]; - binderConfiguration.setBrokers(bAddresses.toArray(foo)); - return binderConfiguration; - } - - private int partitionSize(String topic) { - return consumerFactory().createConsumer().partitionsFor(topic).size(); - } - - private void invokeCreateTopic(String topic, int partitions, int replicationFactor) - throws Exception { - - NewTopic newTopic = new NewTopic(topic, partitions, (short) replicationFactor); - CreateTopicsResult topics = adminClient - .createTopics(Collections.singletonList(newTopic)); - topics.all().get(DEFAULT_OPERATION_TIMEOUT, TimeUnit.SECONDS); - } - - private String getKafkaOffsetHeaderKey() { - return KafkaHeaders.OFFSET; - } - - @BeforeEach - public void init() { - String multiplier = System.getenv("KAFKA_TIMEOUT_MULTIPLIER"); - if (multiplier != null) { - timeoutMultiplier = Double.parseDouble(multiplier); - } - - BrokerAddress[] brokerAddresses = embeddedKafka.getBrokerAddresses(); - List bAddresses = new ArrayList<>(); - for (BrokerAddress bAddress : brokerAddresses) { - bAddresses.add(bAddress.toString()); - } - String[] foo = new String[bAddresses.size()]; - - Map adminConfigs = new HashMap<>(); - adminConfigs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, - bAddresses.toArray(foo)[0]); - adminClient = AdminClient.create(adminConfigs); - } - - private int invokePartitionSize(String topic) throws Throwable { - - DescribeTopicsResult describeTopicsResult = adminClient - .describeTopics(Collections.singletonList(topic)); - KafkaFuture> all = describeTopicsResult.all(); - Map stringTopicDescriptionMap = all - .get(DEFAULT_OPERATION_TIMEOUT, TimeUnit.SECONDS); - TopicDescription topicDescription = stringTopicDescriptionMap.get(topic); - return topicDescription.partitions().size(); - } - - @Override - protected boolean usesExplicitRouting() { - return false; - } - - @Override - protected String getClassUnderTestName() { - return CLASS_UNDER_TEST_NAME; - } - - @Override - public Spy spyOn(final String name) { - throw new UnsupportedOperationException("'spyOn' is not used by Kafka tests"); - } - - private ConsumerFactory consumerFactory() { - Map props = new HashMap<>(); - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, - configurationProperties.getKafkaConnectionString()); - props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); - props.put(ConsumerConfig.GROUP_ID_CONFIG, "TEST-CONSUMER-GROUP"); - Deserializer valueDecoder = new ByteArrayDeserializer(); - Deserializer keyDecoder = new ByteArrayDeserializer(); - - return new DefaultKafkaConsumerFactory<>(props, keyDecoder, valueDecoder); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void bindersAdmin() throws Exception { - KafkaBinderConfigurationProperties props = createConfigurationProperties(); - props.getConfiguration().put(AdminClientConfig.CLIENT_ID_CONFIG, "binder"); - props.setEnableObservation(true); - Binder binder = getBinder(props); - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("admin.0", - moduleOutputChannel, producerBindingProperties.getProducer()); - - Binding consumerBinding = binder.bindConsumer("admin.0", - "testSendAndReceiveNoOriginalContentType", moduleInputChannel, - consumerProperties); - - assertThat( - KafkaTestUtils.getPropertyValue(producerBinding, "lifecycle.kafkaTemplate.kafkaAdmin", KafkaAdmin.class) - .getConfigurationProperties()).contains(entry(AdminClientConfig.CLIENT_ID_CONFIG, "binder")); - assertThat(KafkaTestUtils - .getPropertyValue(consumerBinding, "lifecycle.messageListenerContainer.kafkaAdmin", KafkaAdmin.class) - .getConfigurationProperties()).contains(entry(AdminClientConfig.CLIENT_ID_CONFIG, "binder")); - - consumerBinding.unbind(); - producerBinding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testDefaultHeaderMapper() throws Exception { - Binder binder = getBinder(); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension() - .setTrustedPackages(new String[] { "org.springframework.cloud.stream.binder.kafka" }); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("bar.0", - moduleOutputChannel, producerBindingProperties.getProducer()); - - Binding consumerBinding = binder.bindConsumer("bar.0", - "testSendAndReceiveNoOriginalContentType", moduleInputChannel, - consumerProperties); - binderBindUnbindLatency(); - - final Pojo pojoHeader = new Pojo("testing"); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .setHeader("foo", pojoHeader).build(); - - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - Assertions.assertThat(inboundMessageRef.get()).isNotNull(); - Assertions.assertThat(inboundMessageRef.get().getPayload()) - .isEqualTo("foo".getBytes()); - Assertions - .assertThat(inboundMessageRef.get().getHeaders() - .get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.TEXT_PLAIN); - Assertions.assertThat(inboundMessageRef.get().getHeaders().get("foo")) - .isInstanceOf(Pojo.class); - Pojo actual = (Pojo) inboundMessageRef.get().getHeaders().get("foo"); - Assertions.assertThat(actual.field).isEqualTo(pojoHeader.field); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testCustomHeaderMapper() throws Exception { - - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - binderConfiguration.setHeaderMapperBeanName("headerMapper"); - - KafkaTopicProvisioner kafkaTopicProvisioner = new KafkaTopicProvisioner( - binderConfiguration, new TestKafkaProperties(), prop -> { - }); - try { - kafkaTopicProvisioner.afterPropertiesSet(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - KafkaTestBinder binder = new KafkaTestBinder(binderConfiguration, kafkaTopicProvisioner); - ((GenericApplicationContext) binder.getApplicationContext()).registerBean("headerMapper", - KafkaHeaderMapper.class, () -> new KafkaHeaderMapper() { - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - target.add(new RecordHeader("custom-header", "foobar".getBytes())); - } - - @Override - public void toHeaders(Headers source, Map target) { - if (source.headers("custom-header").iterator().hasNext()) { - target.put("custom-header", source.headers("custom-header").iterator().next().value()); - } - - } - }); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("bar.0", - moduleOutputChannel, producerProperties); - - Binding consumerBinding = binder.bindConsumer("bar.0", - "testSendAndReceiveNoOriginalContentType", moduleInputChannel, - consumerProperties); - binderBindUnbindLatency(); - - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo") - .setHeader("foo", MimeTypeUtils.TEXT_PLAIN).build(); - - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - Assertions.assertThat(inboundMessageRef.get()).isNotNull(); - Assertions.assertThat(inboundMessageRef.get().getPayload()) - .isEqualTo("foo".getBytes()); - Assertions.assertThat(inboundMessageRef.get().getHeaders().get("foo")) - .isNull(); - Assertions.assertThat(inboundMessageRef.get().getHeaders().get("custom-header")) - .isEqualTo("foobar".getBytes()); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testWellKnownHeaderMapperWithBeanNameKafkaHeaderMapper() throws Exception { - - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - - KafkaTopicProvisioner kafkaTopicProvisioner = new KafkaTopicProvisioner( - binderConfiguration, new TestKafkaProperties(), prop -> { - }); - try { - kafkaTopicProvisioner.afterPropertiesSet(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - KafkaTestBinder binder = new KafkaTestBinder(binderConfiguration, kafkaTopicProvisioner); - ((GenericApplicationContext) binder.getApplicationContext()).registerBean("kafkaBinderHeaderMapper", - KafkaHeaderMapper.class, () -> new BinderHeaderMapper() { - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - target.add(new RecordHeader("custom-header", "foobar".getBytes())); - super.fromHeaders(headers, target); - } - - @Override - public void toHeaders(Headers source, Map target) { - if (source.headers("custom-header").iterator().hasNext()) { - target.put("custom-header", source.headers("custom-header").iterator().next().value()); - } - - } - }); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("bar.0", - moduleOutputChannel, producerProperties); - - Binding consumerBinding = binder.bindConsumer("bar.0", - "testSendAndReceiveNoOriginalContentType", moduleInputChannel, - consumerProperties); - binderBindUnbindLatency(); - - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo") - .setHeader("foo", MimeTypeUtils.TEXT_PLAIN).build(); - - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - Assertions.assertThat(inboundMessageRef.get()).isNotNull(); - Assertions.assertThat(inboundMessageRef.get().getPayload()) - .isEqualTo("foo".getBytes()); - Assertions.assertThat(inboundMessageRef.get().getHeaders().get("foo")) - .isNull(); - Assertions.assertThat(inboundMessageRef.get().getHeaders().get("custom-header")) - .isEqualTo("foobar".getBytes()); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @Override - @SuppressWarnings("unchecked") - public void testSendAndReceiveNoOriginalContentType(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - Binding producerBinding = binder.bindProducer("bar.0", - moduleOutputChannel, producerBindingProperties.getProducer()); - - consumerProperties.getExtension() - .setTrustedPackages(new String[] { "org.springframework.util" }); - Binding consumerBinding = binder.bindConsumer("bar.0", - "testSendAndReceiveNoOriginalContentType", moduleInputChannel, - consumerProperties); - binderBindUnbindLatency(); - - // TODO: Will have to fix the MimeType to convert to byte array once this issue - // has been resolved: - // https://github.com/spring-projects/spring-kafka/issues/424 - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat(inboundMessageRef.get().getPayload()).isEqualTo("foo".getBytes()); - assertThat(inboundMessageRef.get().getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.TEXT_PLAIN); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @Override - @SuppressWarnings("unchecked") - public void testSendAndReceive(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - BindingProperties outputBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - MessagingMessageConverter mmc = new MessagingMessageConverter(); - ((GenericApplicationContext) ((KafkaTestBinder) binder).getApplicationContext()) - .registerBean("tSARmmc", MessagingMessageConverter.class, () -> mmc); - consumerProperties.getExtension().setConverterBeanName("tSARmmc"); - - Binding producerBinding = binder.bindProducer("foo.bar", - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("foo.bar", - "testSendAndReceive", moduleInputChannel, consumerProperties); - assertThat(KafkaTestUtils.getPropertyValue(consumerBinding, - "lifecycle.messageListenerContainer.applicationContext")).isNotNull(); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo".getBytes(StandardCharsets.UTF_8)) - .setHeader(MessageHeaders.CONTENT_TYPE, - MimeTypeUtils.APPLICATION_OCTET_STREAM) - .build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat( - new String(inboundMessageRef.get().getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("foo"); - assertThat(inboundMessageRef.get().getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_OCTET_STREAM); - - Map topicsInUse = ((KafkaTestBinder) binder) - .getCoreBinder().getTopicsInUse(); - assertThat(topicsInUse.keySet()).contains("foo.bar"); - TopicInformation topic = topicsInUse.get("foo.bar"); - assertThat(topic.isConsumerTopic()).isTrue(); - assertThat(topic.consumerGroup()).isEqualTo("testSendAndReceive"); - - assertThat(KafkaTestUtils.getPropertyValue(consumerBinding, "lifecycle.recordListener.messageConverter")) - .isSameAs(mmc); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testSendAndReceiveBatch() throws Exception { - Binder binder = getBinder(); - BindingProperties outputBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setBatchMode(true); - consumerProperties.getExtension().getConfiguration().put("fetch.min.bytes", "1000"); - consumerProperties.getExtension().getConfiguration().put("fetch.max.wait.ms", "5000"); - consumerProperties.getExtension().getConfiguration().put("max.poll.records", "2"); - BatchMessagingMessageConverter bmmc = new BatchMessagingMessageConverter(); - ((GenericApplicationContext) ((KafkaTestBinder) binder).getApplicationContext()) - .registerBean("tSARBbmmc", BatchMessagingMessageConverter.class, () -> bmmc); - consumerProperties.getExtension().setConverterBeanName("tSARBbmmc"); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("c.batching", - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("c.batching", - "testSendAndReceiveBatch", moduleInputChannel, consumerProperties); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo".getBytes(StandardCharsets.UTF_8)) - .setHeader(KafkaHeaders.PARTITION, 0) - .build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - message = MessageBuilder - .withPayload("bar".getBytes(StandardCharsets.UTF_8)) - .setHeader(KafkaHeaders.PARTITION, 0) - .build(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference>> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.compareAndSet(null, (Message>) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - List payload = inboundMessageRef.get().getPayload(); - assertThat(payload.get(0)).isEqualTo("foo".getBytes()); - if (payload.size() > 1) { // it's a race as to whether we'll get them both or just one. - assertThat(payload.get(1)).isEqualTo("bar".getBytes()); - } - - assertThat(KafkaTestUtils.getPropertyValue(consumerBinding, "lifecycle.batchListener.batchMessageConverter")) - .isSameAs(bmmc); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testSendAndReceiveBatchWithDlqEnabled() throws Exception { - Binder binder = getBinder(); - BindingProperties outputBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setBatchMode(true); - consumerProperties.getExtension().setEnableDlq(true); - consumerProperties.getExtension().setDlqName("tsarbwde-dlq-topic"); - consumerProperties.getExtension().getConfiguration().put("fetch.min.bytes", "1000"); - consumerProperties.getExtension().getConfiguration().put("fetch.max.wait.ms", "5000"); - consumerProperties.getExtension().getConfiguration().put("max.poll.records", "2"); - BatchMessagingMessageConverter bmmc = new BatchMessagingMessageConverter(); - ((GenericApplicationContext) ((KafkaTestBinder) binder).getApplicationContext()) - .registerBean("tSARBbmmc", BatchMessagingMessageConverter.class, () -> bmmc); - consumerProperties.getExtension().setConverterBeanName("tSARBbmmc"); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("tsarbwde.batching", - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("tsarbwde.batching", - "testSendAndReceiveBatch", moduleInputChannel, consumerProperties); - - QueueChannel dlqChannel = new QueueChannel(); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - Binding dlqConsumerBinding = binder.bindConsumer( - "tsarbwde-dlq-topic", null, dlqChannel, - dlqConsumerProperties); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - - String testMessagePayload = "test." + UUID.randomUUID(); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload(testMessagePayload.getBytes(StandardCharsets.UTF_8)) - .setHeader(KafkaHeaders.PARTITION, 0) - .build(); - - moduleOutputChannel.send(message); - - Message receivedMessage = receive(dlqChannel, 10); - assertThat(receivedMessage).isNotNull(); - assertThat(receivedMessage.getPayload()).isEqualTo(testMessagePayload.getBytes()); - - producerBinding.unbind(); - consumerBinding.unbind(); - dlqConsumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testDlqWithNativeSerializationEnabledOnDlqProducer() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - - // Native serialization for producer - producerProperties.setUseNativeEncoding(true); - Map producerConfig = new HashMap<>(); - producerConfig.put("value.serializer", - "org.apache.kafka.common.serialization.StringSerializer"); - producerProperties.getExtension().setConfiguration(producerConfig); - - BindingProperties outputBindingProperties = createProducerBindingProperties( - producerProperties); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - // Native Deserialization for consumer - consumerProperties.setUseNativeDecoding(true); - Map consumerConfig = new HashMap<>(); - consumerConfig.put("value.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - consumerProperties.getExtension().setConfiguration(consumerConfig); - - // Setting dlq producer properties on the consumer - consumerProperties.getExtension() - .setDlqProducerProperties(producerProperties.getExtension()); - consumerProperties.getExtension().setEnableDlq(true); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("foo.bar", - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("foo.bar", - "testDlqWithNativeEncoding-1", moduleInputChannel, consumerProperties); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - - // Consumer for the DLQ destination - QueueChannel dlqChannel = new QueueChannel(); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - - Binding dlqConsumerBinding = binder.bindConsumer( - "error.foo.bar." + "testDlqWithNativeEncoding-1", null, dlqChannel, - dlqConsumerProperties); - binderBindUnbindLatency(); - - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo").build(); - - moduleOutputChannel.send(message); - Message receivedMessage = receive(dlqChannel, 5); - assertThat(receivedMessage).isNotNull(); - assertThat(receivedMessage.getPayload()).isEqualTo("foo".getBytes()); - Awaitility.await().until(() -> handler.getInvocationCount() == consumerProperties.getMaxAttempts()); - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TOPIC)) - .isEqualTo("foo.bar".getBytes(StandardCharsets.UTF_8)); - assertThat(new String((byte[]) receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_MESSAGE))).startsWith( - "Dispatcher failed to deliver Message"); - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_STACKTRACE)).isNotNull(); - binderBindUnbindLatency(); - - dlqConsumerBinding.unbind(); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testDlqWithNativeDecodingOnConsumerButMissingSerializerOnDlqProducer() - throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - // Native serialization for producer - producerProperties.setUseNativeEncoding(true); - Map producerConfig = new HashMap<>(); - producerConfig.put("value.serializer", - "org.apache.kafka.common.serialization.StringSerializer"); - producerProperties.getExtension().setConfiguration(producerConfig); - BindingProperties outputBindingProperties = createProducerBindingProperties( - producerProperties); - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - // Native Deserialization for consumer - consumerProperties.setUseNativeDecoding(true); - - Map consumerConfig = new HashMap<>(); - consumerConfig.put("value.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - - // No Dlq producer properties set on the consumer with a native serializer. - // This should cause an error for DLQ sending. - - consumerProperties.getExtension().setConfiguration(consumerConfig); - consumerProperties.getExtension().setEnableDlq(true); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("foo.bar", - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("foo.bar", - "testDlqWithNativeEncoding-2", moduleInputChannel, consumerProperties); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - - // Consumer for the DLQ destination - QueueChannel dlqChannel = new QueueChannel(); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - - Binding dlqConsumerBinding = binder.bindConsumer( - "error.foo.bar." + "testDlqWithNativeEncoding-2", null, dlqChannel, - dlqConsumerProperties); - binderBindUnbindLatency(); - - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo").build(); - - moduleOutputChannel.send(message); - - Message receivedMessage = dlqChannel.receive(5000); - // Ensure that we didn't receive anything on DLQ because of serializer config - // missing on dlq producer while native Decoding is enabled. - assertThat(receivedMessage).isNull(); - - binderBindUnbindLatency(); - - dlqConsumerBinding.unbind(); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - // For more details on the context of this test: https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/657 - @Test - @SuppressWarnings("unchecked") - void testDlqWithProducerPropertiesSetAtBinderLevel() - throws Exception { - - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - - Map consumerProps = new HashMap<>(); - consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); - binderConfiguration.setConsumerProperties(consumerProps); - - Map producerProps = new HashMap<>(); - producerProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); - binderConfiguration.setProducerProperties(producerProps); - Binder binder = getBinder(binderConfiguration); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setUseNativeEncoding(true); - BindingProperties outputBindingProperties = createProducerBindingProperties( - producerProperties); - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setUseNativeDecoding(true); - consumerProperties.getExtension().setEnableDlq(true); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("foo.bar", - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("foo.bar", - "tdwcapsabl", moduleInputChannel, consumerProperties); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - - // Consumer for the DLQ destination - QueueChannel dlqChannel = new QueueChannel(); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - - Binding dlqConsumerBinding = binder.bindConsumer( - "error.foo.bar." + "tdwcapsabl", null, dlqChannel, - dlqConsumerProperties); - binderBindUnbindLatency(); - - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("foo").build(); - - moduleOutputChannel.send(message); - - Message receivedMessage = dlqChannel.receive(5000); - assertThat(receivedMessage).isNotNull(); - assertThat(receivedMessage.getPayload()).isEqualTo("foo"); - - binderBindUnbindLatency(); - - dlqConsumerBinding.unbind(); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - void testDlqAndRetry() throws Exception { - testDlqGuts(true, null, null, false, false); - } - - @Test - void testDlqAndRetryTransactional() throws Exception { - testDlqGuts(true, null, null, true, false); - } - - @Test - void testDlq() throws Exception { - testDlqGuts(false, null, 3, false, false); - } - - @Test - void testDlqWithDlqDestinationResolver() throws Exception { - testDlqGuts(false, null, 3, false, true); - } - - @Test - void testDlqTransactional() throws Exception { - testDlqGuts(false, null, 3, true, false); - } - - @Test - void testDlqNone() throws Exception { - testDlqGuts(false, HeaderMode.none, 1, false, false); - } - - @Test - void testDlqEmbedded() throws Exception { - testDlqGuts(false, HeaderMode.embeddedHeaders, 3, false, false); - } - - private void testDlqGuts(boolean withRetry, HeaderMode headerMode, Integer dlqPartitions, - boolean transactional, boolean useDlqDestResolver) throws Exception { - - int expectedDlqPartition = dlqPartitions == null ? 0 : dlqPartitions - 1; - KafkaBinderConfigurationProperties binderConfig = createConfigurationProperties(); - if (transactional) { - binderConfig.getTransaction().setTransactionIdPrefix("tx-"); - binderConfig.getTransaction().getProducer().getConfiguration().put(ProducerConfig.RETRIES_CONFIG, "1"); - binderConfig.setRequiredAcks("all"); - } - DlqPartitionFunction dlqPartitionFunction; - if (Integer.valueOf(1).equals(dlqPartitions)) { - dlqPartitionFunction = null; // test that ZERO_PARTITION is used - } - else if (dlqPartitions == null) { - dlqPartitionFunction = (group, rec, ex) -> 0; - } - else { - dlqPartitionFunction = (group, rec, ex) -> dlqPartitions - 1; - } - DlqDestinationResolver dlqDestinationResolver = null; - if (useDlqDestResolver) { - dlqDestinationResolver = (cr, e) -> "foo.dlq"; - } - AbstractKafkaTestBinder binder = getBinder(binderConfig, dlqPartitionFunction, dlqDestinationResolver); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension() - .setHeaderPatterns(new String[] { MessageHeaders.CONTENT_TYPE }); - producerProperties.setHeaderMode(headerMode); - producerProperties.setPartitionCount(2); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - consumerProperties.setMaxAttempts(withRetry ? 2 : 1); - consumerProperties.setBackOffInitialInterval(100); - consumerProperties.setBackOffMaxInterval(150); - consumerProperties.getExtension().setEnableDlq(true); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - consumerProperties.setHeaderMode(headerMode); - consumerProperties.setMultiplex(true); - consumerProperties.getExtension().setDlqPartitions(dlqPartitions); - consumerProperties.setConcurrency(2); - consumerProperties.populateBindingName("foobar"); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - QueueChannel dlqChannel = new QueueChannel(); - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - - long uniqueBindingId = System.currentTimeMillis(); - - String producerName = "dlqTest." + uniqueBindingId + ".0"; - Binding producerBinding = binder.bindProducer(producerName, - moduleOutputChannel, producerProperties); - String consumerDest = producerName + ", " + producerName.replaceAll("0", "1"); - Binding consumerBinding = binder.bindConsumer(consumerDest, - "testGroup", moduleInputChannel, consumerProperties); - - AbstractMessageListenerContainer container = TestUtils.getPropertyValue(consumerBinding, - "lifecycle.messageListenerContainer", AbstractMessageListenerContainer.class); - assertThat(container.getContainerProperties().getTopicPartitions().length) - .isEqualTo(4); // 2 topics 2 partitions each - if (transactional) { - assertThat(TestUtils.getPropertyValue(container.getAfterRollbackProcessor(), "kafkaTemplate")).isNotNull(); - assertThat( - TestUtils.getPropertyValue(container.getAfterRollbackProcessor(), "commitRecovered", Boolean.class)) - .isTrue(); - } - - String dlqTopic = useDlqDestResolver ? "foo.dlq" : "error.dlqTest." + uniqueBindingId + ".0.testGroup"; - try (AdminClient admin = AdminClient.create(Collections.singletonMap(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, - embeddedKafka.getBrokersAsString()))) { - if (useDlqDestResolver) { - List nonProvisionedDlqTopics = new ArrayList<>(); - NewTopic nTopic = new NewTopic(dlqTopic, 3, (short) 1); - nonProvisionedDlqTopics.add(nTopic); - admin.createTopics(nonProvisionedDlqTopics); - } - Map topicDescriptions = admin.describeTopics(Collections.singletonList(dlqTopic)) - .all() - .get(10, TimeUnit.SECONDS); - assertThat(topicDescriptions).hasSize(1); - assertThat(topicDescriptions.values().iterator().next().partitions()) - .hasSize(dlqPartitions == null ? 2 : dlqPartitions); - } - - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - dlqConsumerProperties.setHeaderMode(headerMode); - dlqConsumerProperties.getExtension().setTxCommitRecovered(false); - - ApplicationContext context = TestUtils.getPropertyValue(binder.getBinder(), - "applicationContext", ApplicationContext.class); - SubscribableChannel boundErrorChannel = context.getBean(binder.getBinder().getBinderIdentity() + ".foobar.errors-0", SubscribableChannel.class); -// SubscribableChannel boundErrorChannel = context -// .getBean(consumerDest + ".errors-0", SubscribableChannel.class); - SubscribableChannel globalErrorChannel = context.getBean("errorChannel", - SubscribableChannel.class); - final AtomicReference> boundErrorChannelMessage = new AtomicReference<>(); - final AtomicReference> globalErrorChannelMessage = new AtomicReference<>(); - final AtomicBoolean hasRecovererInCallStack = new AtomicBoolean(!withRetry); - final AtomicBoolean hasAfterRollbackProcessorInStack = new AtomicBoolean(!withRetry); - boundErrorChannel.subscribe(message -> { - boundErrorChannelMessage.set(message); - String stackTrace = Arrays.toString(new RuntimeException().getStackTrace()); - hasRecovererInCallStack - .set(stackTrace.contains("ErrorMessageSendingRecoverer")); - hasAfterRollbackProcessorInStack.set(stackTrace.contains("DefaultAfterRollbackProcessor")); - }); - globalErrorChannel.subscribe(globalErrorChannelMessage::set); - - Binding dlqConsumerBinding = binder.bindConsumer( - dlqTopic, null, dlqChannel, - dlqConsumerProperties); - binderBindUnbindLatency(); - if (transactional) { - assertThat(TestUtils.getPropertyValue(dlqConsumerBinding, - "lifecycle.messageListenerContainer.afterRollbackProcessor.kafkaTemplate")).isNotNull(); - assertThat( - TestUtils.getPropertyValue(dlqConsumerBinding, - "lifecycle.messageListenerContainer.afterRollbackProcessor.commitRecovered", Boolean.class)) - .isFalse(); - } - - String testMessagePayload = "test." + UUID.randomUUID().toString(); - Message testMessage = MessageBuilder - .withPayload(testMessagePayload.getBytes()) - .setHeader(KafkaHeaders.PARTITION, 1) - .build(); - moduleOutputChannel.send(testMessage); - - Message receivedMessage = receive(dlqChannel, 3); - assertThat(receivedMessage).isNotNull(); - assertThat(receivedMessage.getPayload()).isEqualTo(testMessagePayload.getBytes()); - if (HeaderMode.embeddedHeaders.equals(headerMode)) { - assertThat(handler.getInvocationCount()) - .isEqualTo(consumerProperties.getMaxAttempts()); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TOPIC)) - .isEqualTo(producerName); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_PARTITION)).isEqualTo(1); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_OFFSET)).isEqualTo(0); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TIMESTAMP)).isNotNull(); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TIMESTAMP_TYPE)) - .isEqualTo(TimestampType.CREATE_TIME.toString()); - - assertThat(((String) receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_MESSAGE))).startsWith( - "Dispatcher failed to deliver Message"); - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_STACKTRACE)).isNotNull(); - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_FQCN)).isNotNull(); - assertThat(receivedMessage.getHeaders() - .get(KafkaHeaders.RECEIVED_PARTITION)).isEqualTo(expectedDlqPartition); - } - else if (!HeaderMode.none.equals(headerMode)) { - assertThat(handler.getInvocationCount()) - .isEqualTo(consumerProperties.getMaxAttempts()); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TOPIC)) - .isEqualTo(producerName.getBytes(StandardCharsets.UTF_8)); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_PARTITION)).isEqualTo( - ByteBuffer.allocate(Integer.BYTES).putInt(1).array()); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_OFFSET)).isEqualTo( - ByteBuffer.allocate(Long.BYTES).putLong(0).array()); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TIMESTAMP)).isNotNull(); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TIMESTAMP_TYPE)) - .isEqualTo(TimestampType.CREATE_TIME.toString().getBytes()); - - if (transactional) { - assertThat(new String((byte[]) receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_MESSAGE))).startsWith( - "Transaction rollback limit exceeded"); - } - else { - assertThat(new String((byte[]) receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_MESSAGE))).startsWith( - "Dispatcher failed to deliver Message"); - } - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_STACKTRACE)).isNotNull(); - - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_EXCEPTION_FQCN)).isNotNull(); - - assertThat(receivedMessage.getHeaders() - .get(KafkaHeaders.RECEIVED_PARTITION)).isEqualTo(expectedDlqPartition); - } - else { - assertThat(receivedMessage.getHeaders() - .get(KafkaMessageChannelBinder.X_ORIGINAL_TOPIC)).isNull(); - } - binderBindUnbindLatency(); - - // verify we got a message on the dedicated error channel and the global (via - // bridge) - assertThat(boundErrorChannelMessage.get()).isNotNull(); -// assertThat(globalErrorChannelMessage.get()).isNotNull(); -// assertThat(hasRecovererInCallStack.get()).isEqualTo(withRetry && !transactional); - assertThat(hasAfterRollbackProcessorInStack.get()).isEqualTo(transactional); - - dlqConsumerBinding.unbind(); - consumerBinding.unbind(); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testDefaultAutoCommitOnErrorWithDlq() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - producerProperties); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setMaxAttempts(3); - consumerProperties.setBackOffInitialInterval(100); - consumerProperties.setBackOffMaxInterval(150); - consumerProperties.getExtension().setEnableDlq(true); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "retryTest." + uniqueBindingId + ".0", moduleOutputChannel, - producerProperties); - Binding consumerBinding = binder.bindConsumer( - "retryTest." + uniqueBindingId + ".0", "testGroup", moduleInputChannel, - consumerProperties); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - QueueChannel dlqChannel = new QueueChannel(); - Binding dlqConsumerBinding = binder.bindConsumer( - "error.retryTest." + uniqueBindingId + ".0.testGroup", null, dlqChannel, - dlqConsumerProperties); - - String testMessagePayload = "test." + UUID.randomUUID().toString(); - Message testMessage = MessageBuilder - .withPayload(testMessagePayload.getBytes()).build(); - moduleOutputChannel.send(testMessage); - - Message dlqMessage = receive(dlqChannel, 3); - assertThat(dlqMessage).isNotNull(); - assertThat(dlqMessage.getPayload()).isEqualTo(testMessagePayload.getBytes()); - - // first attempt fails - assertThat(handler.getReceivedMessages().entrySet()).hasSize(1); - Message handledMessage = handler.getReceivedMessages().entrySet().iterator() - .next().getValue(); - assertThat(handledMessage).isNotNull(); - assertThat( - new String((byte[]) handledMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testMessagePayload); - assertThat(handler.getInvocationCount()) - .isEqualTo(consumerProperties.getMaxAttempts()); - binderBindUnbindLatency(); - dlqConsumerBinding.unbind(); - consumerBinding.unbind(); - - // on the second attempt the message is not redelivered because the DLQ is set - QueueChannel successfulInputChannel = new QueueChannel(); - consumerBinding = binder.bindConsumer("retryTest." + uniqueBindingId + ".0", - "testGroup", successfulInputChannel, consumerProperties); - String testMessage2Payload = "test." + UUID.randomUUID().toString(); - Message testMessage2 = MessageBuilder - .withPayload(testMessage2Payload.getBytes()).build(); - moduleOutputChannel.send(testMessage2); - - Message receivedMessage = receive(successfulInputChannel); - assertThat(receivedMessage.getPayload()) - .isEqualTo(testMessage2Payload.getBytes()); - - binderBindUnbindLatency(); - consumerBinding.unbind(); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testRetriesWithoutDlq() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - producerProperties); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setMaxAttempts(2); - consumerProperties.setBackOffInitialInterval(100); - consumerProperties.setBackOffMaxInterval(150); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "retryTest." + uniqueBindingId + ".0", moduleOutputChannel, - producerProperties); - Binding consumerBinding = binder.bindConsumer( - "retryTest." + uniqueBindingId + ".0", "testGroup", moduleInputChannel, - consumerProperties); - - String testMessagePayload = "test." + UUID.randomUUID(); - Message testMessage = MessageBuilder - .withPayload(testMessagePayload.getBytes()).build(); - moduleOutputChannel.send(testMessage); - - Thread.sleep(3000); - - // Since we don't have a DLQ, assert that we are invoking the handler exactly the same number of times - // as set in consumerProperties.maxAttempt and not the default set by Spring Kafka (10 times). - assertThat(handler.getInvocationCount()) - .isEqualTo(consumerProperties.getMaxAttempts()); - binderBindUnbindLatency(); - consumerBinding.unbind(); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testCommonErrorHandlerBeanNameOnConsumerBinding() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - producerProperties); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - CountDownLatch latch = new CountDownLatch(1); - CommonErrorHandler commonErrorHandler = new DefaultErrorHandler(new FixedBackOff(0L, 0L)) { - @Override - public void handleRemaining(Exception thrownException, List> records, - Consumer consumer, MessageListenerContainer container) { - super.handleRemaining(thrownException, records, consumer, container); - latch.countDown(); - } - }; - - ConfigurableApplicationContext context = TestUtils.getPropertyValue(binder, - "binder.applicationContext", ConfigurableApplicationContext.class); - context.getBeanFactory().registerSingleton("fooCommonErrorHandler", commonErrorHandler); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setMaxAttempts(2); - consumerProperties.setBackOffInitialInterval(100); - consumerProperties.setBackOffMaxInterval(150); - consumerProperties.getExtension().setCommonErrorHandlerBeanName("fooCommonErrorHandler"); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "retryTest." + uniqueBindingId + ".0", moduleOutputChannel, - producerProperties); - Binding consumerBinding = binder.bindConsumer( - "retryTest." + uniqueBindingId + ".0", "testGroup", moduleInputChannel, - consumerProperties); - - String testMessagePayload = "test." + UUID.randomUUID(); - Message testMessage = MessageBuilder - .withPayload(testMessagePayload.getBytes()).build(); - moduleOutputChannel.send(testMessage); - - Thread.sleep(3000); - - //Assertions for the CommonErrorHandler configured on the consumer binding (commonErrorHandlerBeanName). - assertThat(KafkaTestUtils.getPropertyValue(consumerBinding, - "lifecycle.messageListenerContainer.commonErrorHandler")).isSameAs(commonErrorHandler); - latch.await(10, TimeUnit.SECONDS); - - binderBindUnbindLatency(); - consumerBinding.unbind(); - producerBinding.unbind(); - } - - - //See https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/870 for motivation for this test. - @Test - @SuppressWarnings("unchecked") - void testAutoCommitOnErrorWhenManualAcknowledgement() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - producerProperties); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setMaxAttempts(3); - consumerProperties.setBackOffInitialInterval(100); - consumerProperties.setBackOffMaxInterval(150); - //When auto commit is disabled, then the record is committed after publishing to DLQ using the manual acknowledgement. - // (if DLQ is enabled, which is, in this case). - consumerProperties.getExtension().setAutoCommitOffset(false); - consumerProperties.getExtension().setEnableDlq(true); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "retryTest." + uniqueBindingId + ".0", moduleOutputChannel, - producerProperties); - Binding consumerBinding = binder.bindConsumer( - "retryTest." + uniqueBindingId + ".0", "testGroup", moduleInputChannel, - consumerProperties); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - QueueChannel dlqChannel = new QueueChannel(); - Binding dlqConsumerBinding = binder.bindConsumer( - "error.retryTest." + uniqueBindingId + ".0.testGroup", null, dlqChannel, - dlqConsumerProperties); - - String testMessagePayload = "test." + UUID.randomUUID().toString(); - Message testMessage = MessageBuilder - .withPayload(testMessagePayload.getBytes()).build(); - moduleOutputChannel.send(testMessage); - - Message dlqMessage = receive(dlqChannel, 3); - assertThat(dlqMessage).isNotNull(); - assertThat(dlqMessage.getPayload()).isEqualTo(testMessagePayload.getBytes()); - - // first attempt fails - assertThat(handler.getReceivedMessages().entrySet()).hasSize(1); - Message handledMessage = handler.getReceivedMessages().entrySet().iterator() - .next().getValue(); - assertThat(handledMessage).isNotNull(); - assertThat( - new String((byte[]) handledMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testMessagePayload); - assertThat(handler.getInvocationCount()) - .isEqualTo(consumerProperties.getMaxAttempts()); - binderBindUnbindLatency(); - dlqConsumerBinding.unbind(); - consumerBinding.unbind(); - - // on the second attempt the message is not redelivered because the DLQ is set and the record in error is already committed. - QueueChannel successfulInputChannel = new QueueChannel(); - consumerBinding = binder.bindConsumer("retryTest." + uniqueBindingId + ".0", - "testGroup", successfulInputChannel, consumerProperties); - String testMessage2Payload = "test1." + UUID.randomUUID().toString(); - Message testMessage2 = MessageBuilder - .withPayload(testMessage2Payload.getBytes()).build(); - moduleOutputChannel.send(testMessage2); - - Message receivedMessage = receive(successfulInputChannel); - assertThat(receivedMessage.getPayload()) - .isEqualTo(testMessage2Payload.getBytes()); - - binderBindUnbindLatency(); - consumerBinding.unbind(); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testConfigurableDlqName() throws Exception { - Binder binder = getBinder(); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setMaxAttempts(3); - consumerProperties.setBackOffInitialInterval(100); - consumerProperties.setBackOffMaxInterval(150); - consumerProperties.getExtension().setEnableDlq(true); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - String dlqName = "dlqTest"; - consumerProperties.getExtension().setDlqName(dlqName); - BindingProperties producerBindingProperties = createProducerBindingProperties( - producerProperties); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - FailingInvocationCountingMessageHandler handler = new FailingInvocationCountingMessageHandler(); - moduleInputChannel.subscribe(handler); - - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "retryTest." + uniqueBindingId + ".0", moduleOutputChannel, - producerProperties); - Binding consumerBinding = binder.bindConsumer( - "retryTest." + uniqueBindingId + ".0", "testGroup", moduleInputChannel, - consumerProperties); - ExtendedConsumerProperties dlqConsumerProperties = createConsumerProperties(); - dlqConsumerProperties.setMaxAttempts(1); - QueueChannel dlqChannel = new QueueChannel(); - Binding dlqConsumerBinding = binder.bindConsumer(dlqName, null, - dlqChannel, dlqConsumerProperties); - - String testMessagePayload = "test." + UUID.randomUUID().toString(); - Message testMessage = MessageBuilder - .withPayload(testMessagePayload.getBytes()).build(); - moduleOutputChannel.send(testMessage); - - Message dlqMessage = receive(dlqChannel, 3); - assertThat(dlqMessage).isNotNull(); - assertThat(dlqMessage.getPayload()).isEqualTo(testMessagePayload.getBytes()); - - // first attempt fails - assertThat(handler.getReceivedMessages().entrySet()).hasSize(1); - Message handledMessage = handler.getReceivedMessages().entrySet().iterator() - .next().getValue(); - assertThat(handledMessage).isNotNull(); - assertThat( - new String((byte[]) handledMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testMessagePayload); - assertThat(handler.getInvocationCount()) - .isEqualTo(consumerProperties.getMaxAttempts()); - binderBindUnbindLatency(); - dlqConsumerBinding.unbind(); - consumerBinding.unbind(); - - // on the second attempt the message is not redelivered because the DLQ is set - QueueChannel successfulInputChannel = new QueueChannel(); - consumerBinding = binder.bindConsumer("retryTest." + uniqueBindingId + ".0", - "testGroup", successfulInputChannel, consumerProperties); - String testMessage2Payload = "test." + UUID.randomUUID().toString(); - Message testMessage2 = MessageBuilder - .withPayload(testMessage2Payload.getBytes()).build(); - moduleOutputChannel.send(testMessage2); - - Message receivedMessage = receive(successfulInputChannel); - assertThat(receivedMessage.getPayload()) - .isEqualTo(testMessage2Payload.getBytes()); - - binderBindUnbindLatency(); - consumerBinding.unbind(); - producerBinding.unbind(); - } - - @Test - public void testValidateKafkaTopicName() { - try { - KafkaTopicUtils.validateTopicName("foo:bar"); - fail("Expecting IllegalArgumentException"); - } - catch (Exception e) { - // TODO: handle exception - } - } - - @Test - @SuppressWarnings("unchecked") - // TODO: This test needs to be rethought - sending byte[] without explicit content - // type - // - yet being converted by the json converter - void testCompression() throws Exception { - final KafkaProducerProperties.CompressionType[] codecs = new KafkaProducerProperties.CompressionType[] { - KafkaProducerProperties.CompressionType.none, - KafkaProducerProperties.CompressionType.gzip, - KafkaProducerProperties.CompressionType.snappy, - KafkaProducerProperties.CompressionType.zstd}; - byte[] testPayload = new byte[2048]; - Arrays.fill(testPayload, (byte) 65); - Binder binder = getBinder(); - for (KafkaProducerProperties.CompressionType codec : codecs) { - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension().setCompressionType( - KafkaProducerProperties.CompressionType.valueOf(codec.toString())); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer( - "testCompression", moduleOutputChannel, producerProperties); - Binding consumerBinding = binder.bindConsumer( - "testCompression", "test", moduleInputChannel, consumerProperties); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload).build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat(inboundMessageRef.get().getPayload()).containsExactly(testPayload); - producerBinding.unbind(); - consumerBinding.unbind(); - } - } - - @Test - @SuppressWarnings("unchecked") - void testEarliest() throws Exception { - Binding producerBinding = null; - Binding consumerBinding = null; - - try { - Binder binder = getBinder(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel output = createBindableChannel("output", - producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - consumerProperties.getExtension() - .setStartOffset(KafkaConsumerProperties.StartOffset.earliest); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - - DirectChannel input1 = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - String testTopicName = UUID.randomUUID().toString(); - producerBinding = binder.bindProducer(testTopicName, output, - producerBindingProperties.getProducer()); - String testPayload1 = "foo-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload1.getBytes())); - - consumerBinding = binder.bindConsumer(testTopicName, "startOffsets", input1, - consumerProperties); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef1 = new AtomicReference<>(); - MessageHandler messageHandler = message1 -> { - try { - inboundMessageRef1.set((Message) message1); - } - finally { - latch.countDown(); - } - }; - input1.subscribe(messageHandler); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - assertThat(inboundMessageRef1.get()).isNotNull(); - String testPayload2 = "foo-" + UUID.randomUUID().toString(); - input1.unsubscribe(messageHandler); - output.send(new GenericMessage<>(testPayload2.getBytes())); - - CountDownLatch latch1 = new CountDownLatch(1); - AtomicReference> inboundMessageRef2 = new AtomicReference<>(); - input1.subscribe(message1 -> { - try { - inboundMessageRef2.set((Message) message1); - } - finally { - latch1.countDown(); - } - }); - Assert.isTrue(latch1.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef2.get()).isNotNull(); - assertThat(new String(inboundMessageRef2.get().getPayload(), - StandardCharsets.UTF_8)).isEqualTo(testPayload2); - Thread.sleep(2000); - producerBinding.unbind(); - consumerBinding.unbind(); - } - finally { - if (consumerBinding != null) { - consumerBinding.unbind(); - } - if (producerBinding != null) { - producerBinding.unbind(); - } - } - } - - @Test - @Override - @SuppressWarnings("unchecked") - public void testSendAndReceiveMultipleTopics(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - - DirectChannel moduleOutputChannel1 = createBindableChannel("output1", - createProducerBindingProperties(createProducerProperties())); - DirectChannel moduleOutputChannel2 = createBindableChannel("output2", - createProducerBindingProperties(createProducerProperties())); - - QueueChannel moduleInputChannel = new QueueChannel(); - - ExtendedProducerProperties producer1Props = createProducerProperties(); - producer1Props.getExtension().setUseTopicHeader(true); - - Binding producerBinding1 = binder.bindProducer("foo.x", - moduleOutputChannel1, producer1Props); - Binding producerBinding2 = binder.bindProducer("foo.y", - moduleOutputChannel2, createProducerProperties()); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - Binding consumerBinding1 = binder.bindConsumer("foo.x", "test1", - moduleInputChannel, consumerProperties); - Binding consumerBinding2 = binder.bindConsumer("foo.y", "test2", - moduleInputChannel, consumerProperties); - - String testPayload1 = "foo1"; - Message message1 = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload1.getBytes()).build(); - String testPayload2 = "foo2"; - Message message2 = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload2.getBytes()).build(); - String testPayload3 = "foo3"; - Message message3 = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload3.getBytes()) - .setHeader(KafkaHeaders.TOPIC, "foo.y") - .build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel1.send(message1); - moduleOutputChannel2.send(message2); - moduleOutputChannel1.send(message3); - - Message[] messages = new Message[3]; - messages[0] = receive(moduleInputChannel); - messages[1] = receive(moduleInputChannel); - messages[2] = receive(moduleInputChannel); - - assertThat(messages[0]).isNotNull(); - assertThat(messages[1]).isNotNull(); - assertThat(messages[2]).isNotNull(); - assertThat(messages).extracting("payload").containsExactlyInAnyOrder( - testPayload1.getBytes(), testPayload2.getBytes(), testPayload3.getBytes()); - Arrays.asList(messages).forEach(message -> { - if (new String((byte[]) message.getPayload()).equals("foo1")) { - assertThat(message.getHeaders().get(KafkaHeaders.RECEIVED_TOPIC)).isEqualTo("foo.x"); - } - else { - assertThat(message.getHeaders().get(KafkaHeaders.RECEIVED_TOPIC)).isEqualTo("foo.y"); - } - }); - - producerBinding1.unbind(); - producerBinding2.unbind(); - - consumerBinding1.unbind(); - consumerBinding2.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testManualAckSucceedsWhenAutoCommitOffsetIsTurnedOff() throws Exception { - Binder binder = getBinder(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(createProducerProperties())); - QueueChannel moduleInputChannel = new QueueChannel(); - - Binding producerBinding = binder.bindProducer( - "testManualAckSucceedsWhenAutoCommitOffsetIsTurnedOff", - moduleOutputChannel, createProducerProperties()); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAckMode(ContainerProperties.AckMode.MANUAL); - - Binding consumerBinding = binder.bindConsumer( - "testManualAckSucceedsWhenAutoCommitOffsetIsTurnedOff", "test", - moduleInputChannel, consumerProperties); - - String testPayload1 = "foo" + UUID.randomUUID().toString(); - Message message1 = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload1.getBytes()).build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message1); - - Message receivedMessage = receive(moduleInputChannel); - assertThat(receivedMessage).isNotNull(); - assertThat(receivedMessage.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT)) - .isNotNull(); - Acknowledgment acknowledgment = receivedMessage.getHeaders() - .get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class); - try { - acknowledgment.acknowledge(); - } - catch (Exception e) { - fail("Acknowledge must not throw an exception"); - } - finally { - producerBinding.unbind(); - consumerBinding.unbind(); - } - } - - @Test - @SuppressWarnings("unchecked") - void testManualAckIsNotPossibleWhenAutoCommitOffsetIsEnabledOnTheBinder() - throws Exception { - Binder binder = getBinder(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(createProducerProperties())); - QueueChannel moduleInputChannel = new QueueChannel(); - - Binding producerBinding = binder.bindProducer( - "testManualAckIsNotPossibleWhenAutoCommitOffsetIsEnabledOnTheBinder", - moduleOutputChannel, createProducerProperties()); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - Binding consumerBinding = binder.bindConsumer( - "testManualAckIsNotPossibleWhenAutoCommitOffsetIsEnabledOnTheBinder", - "test", moduleInputChannel, consumerProperties); - - AbstractMessageListenerContainer container = TestUtils.getPropertyValue( - consumerBinding, "lifecycle.messageListenerContainer", - AbstractMessageListenerContainer.class); - assertThat(container.getContainerProperties().getAckMode()) - .isEqualTo(ContainerProperties.AckMode.BATCH); - - String testPayload1 = "foo" + UUID.randomUUID().toString(); - Message message1 = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload1.getBytes()).build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message1); - - Message receivedMessage = receive(moduleInputChannel); - assertThat(receivedMessage).isNotNull(); - assertThat(receivedMessage.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT)) - .isNull(); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @Override - @SuppressWarnings("unchecked") - public void testTwoRequiredGroups(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - String testDestination = "testDestination" - + UUID.randomUUID().toString().replace("-", ""); - - producerProperties.setRequiredGroups("test1", "test2"); - Binding producerBinding = binder.bindProducer(testDestination, - output, producerProperties); - - String testPayload = "foo-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload.getBytes())); - - QueueChannel inbound1 = new QueueChannel(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - consumerProperties.getExtension().setAckMode(ContainerProperties.AckMode.RECORD); - Binding consumerBinding1 = binder.bindConsumer(testDestination, - "test1", inbound1, consumerProperties); - QueueChannel inbound2 = new QueueChannel(); - Binding consumerBinding2 = binder.bindConsumer(testDestination, - "test2", inbound2, consumerProperties); - - AbstractMessageListenerContainer container = TestUtils.getPropertyValue( - consumerBinding2, "lifecycle.messageListenerContainer", - AbstractMessageListenerContainer.class); - assertThat(container.getContainerProperties().getAckMode()) - .isEqualTo(ContainerProperties.AckMode.RECORD); - - Message receivedMessage1 = receive(inbound1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String((byte[]) receivedMessage1.getPayload(), - StandardCharsets.UTF_8)).isEqualTo(testPayload); - Message receivedMessage2 = receive(inbound2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String((byte[]) receivedMessage2.getPayload(), - StandardCharsets.UTF_8)).isEqualTo(testPayload); - - consumerBinding1.unbind(); - consumerBinding2.unbind(); - producerBinding.unbind(); - } - - @Test - @Override - @SuppressWarnings("unchecked") - public void testPartitionedModuleSpEL(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setConcurrency(2); - consumerProperties.setInstanceIndex(0); - consumerProperties.setInstanceCount(3); - consumerProperties.setPartitioned(true); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.input0S"); - Binding input0Binding = binder.bindConsumer("part.0", "test", - input0, consumerProperties); - consumerProperties.setInstanceIndex(1); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1S"); - Binding input1Binding = binder.bindConsumer("part.0", "test", - input1, consumerProperties); - consumerProperties.setInstanceIndex(2); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2S"); - Binding input2Binding = binder.bindConsumer("part.0", "test", - input2, consumerProperties); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("payload")); - producerProperties.setPartitionSelectorExpression( - spelExpressionParser.parseExpression("hashCode()")); - producerProperties.setPartitionCount(3); - invokeCreateTopic("output", 6, 1); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("part.0", output, - producerProperties); - try { - Object endpoint = extractEndpoint(outputBinding); - assertThat(getEndpointRouting(endpoint)) - .contains(getExpectedRoutingBaseDestination("part.0", "test") - + "-' + headers['partition']"); - } - catch (UnsupportedOperationException ignored) { - } - List interceptors = output.getInterceptors(); - AtomicInteger count = new AtomicInteger(); - interceptors.forEach(interceptor -> { - if (interceptor instanceof PartitioningInterceptor) { - count.set(TestUtils.getPropertyValue(interceptor, - "partitionHandler.partitionCount", Integer.class)); - } - }); - assertThat(count.get()).isEqualTo(6); - Message message2 = org.springframework.integration.support.MessageBuilder - .withPayload(2) - .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo") - .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42) - .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 43).build(); - output.send(message2); - output.send(new GenericMessage<>(1)); - output.send(new GenericMessage<>(0)); - - Message receive0 = receive(input0); - assertThat(receive0).isNotNull(); - Message receive1 = receive(input1); - assertThat(receive1).isNotNull(); - Message receive2 = receive(input2); - assertThat(receive2).isNotNull(); - - Condition> correlationHeadersForPayload2 = new Condition>() { - - @Override - public boolean matches(Message value) { - IntegrationMessageHeaderAccessor accessor = new IntegrationMessageHeaderAccessor( - value); - return "foo".equals(accessor.getCorrelationId()) - && 42 == accessor.getSequenceNumber() - && 43 == accessor.getSequenceSize(); - } - }; - - ObjectMapper om = new ObjectMapper(); - - if (usesExplicitRouting()) { - assertThat(om.readValue((byte[]) receive0.getPayload(), Integer.class)) - .isEqualTo(0); - assertThat(om.readValue((byte[]) receive1.getPayload(), Integer.class)) - .isEqualTo(1); - assertThat(om.readValue((byte[]) receive2.getPayload(), Integer.class)) - .isEqualTo(2); - assertThat(receive2).has(correlationHeadersForPayload2); - } - else { - List> receivedMessages = Arrays.asList(receive0, receive1, - receive2); - assertThat(receivedMessages).extracting("payload").containsExactlyInAnyOrder( - new byte[] { 48 }, new byte[] { 49 }, new byte[] { 50 }); - Condition> payloadIs2 = new Condition>() { - - @Override - public boolean matches(Message value) { - try { - return om.readValue((byte[]) value.getPayload(), Integer.class) - .equals(2); - } - catch (IOException e) { - // - } - return false; - } - }; - assertThat(receivedMessages).filteredOn(payloadIs2).areExactly(1, - correlationHeadersForPayload2); - - } - input0Binding.unbind(); - input1Binding.unbind(); - input2Binding.unbind(); - outputBinding.unbind(); - } - - @Test -// @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testPartitionedModuleJava() throws Exception { - Binder binder = getBinder(); - - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setConcurrency(2); - consumerProperties.setInstanceCount(4); - consumerProperties.setInstanceIndex(0); - consumerProperties.setPartitioned(true); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.input0J"); - Binding input0Binding = binder.bindConsumer("partJ.0", "test", - input0, consumerProperties); - consumerProperties.setInstanceIndex(1); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1J"); - Binding input1Binding = binder.bindConsumer("partJ.0", "test", - input1, consumerProperties); - consumerProperties.setInstanceIndex(2); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2J"); - Binding input2Binding = binder.bindConsumer("partJ.0", "test", - input2, consumerProperties); - consumerProperties.setInstanceIndex(3); - QueueChannel input3 = new QueueChannel(); - input3.setBeanName("test.input3J"); - Binding input3Binding = binder.bindConsumer("partJ.0", "test", - input3, consumerProperties); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - - this.applicationContext.registerBean("pkExtractor", - PartitionTestSupport.class, () -> new PartitionTestSupport()); - this.applicationContext.registerBean("pkSelector", - PartitionTestSupport.class, () -> new PartitionTestSupport()); - producerProperties.setPartitionKeyExtractorName("pkExtractor"); - producerProperties.setPartitionSelectorName("pkSelector"); - producerProperties.setPartitionCount(3); // overridden to 8 on the actual topic - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("partJ.0", output, - producerProperties); - if (usesExplicitRouting()) { - Object endpoint = extractEndpoint(outputBinding); - assertThat(getEndpointRouting(endpoint)) - .contains(getExpectedRoutingBaseDestination("partJ.0", "test") - + "-' + headers['partition']"); - } - - output.send(new GenericMessage<>(2)); - output.send(new GenericMessage<>(1)); - output.send(new GenericMessage<>(0)); - output.send(new GenericMessage<>(3)); - - Message receive0 = receive(input0); - assertThat(receive0).isNotNull(); - Message receive1 = receive(input1); - assertThat(receive1).isNotNull(); - Message receive2 = receive(input2); - assertThat(receive2).isNotNull(); - Message receive3 = receive(input3); - assertThat(receive3).isNotNull(); - ObjectMapper om = new ObjectMapper(); - - assertThat(om.readValue((byte[]) receive0.getPayload(), Integer.class)) - .isEqualTo(0); - assertThat(om.readValue((byte[]) receive1.getPayload(), Integer.class)) - .isEqualTo(1); - assertThat(om.readValue((byte[]) receive2.getPayload(), Integer.class)) - .isEqualTo(2); - assertThat(om.readValue((byte[]) receive3.getPayload(), Integer.class)) - .isEqualTo(3); - - input0Binding.unbind(); - input1Binding.unbind(); - input2Binding.unbind(); - input3Binding.unbind(); - outputBinding.unbind(); - } - - @Test - @Override - @SuppressWarnings("unchecked") - public void testAnonymousGroup(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel output = createBindableChannel("output", producerBindingProperties); - Binding producerBinding = binder.bindProducer("defaultGroup.0", - output, producerBindingProperties.getProducer()); - - QueueChannel input1 = new QueueChannel(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - Binding binding1 = binder.bindConsumer("defaultGroup.0", null, - input1, consumerProperties); - - QueueChannel input2 = new QueueChannel(); - Binding binding2 = binder.bindConsumer("defaultGroup.0", null, - input2, consumerProperties); - // Since we don't provide any topic info, let Kafka bind the consumer successfully - Thread.sleep(1000); - String testPayload1 = "foo-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload1.getBytes())); - - Message receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload1); - - Message receivedMessage2 = (Message) receive(input2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String(receivedMessage2.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload1); - - binding2.unbind(); - - String testPayload2 = "foo-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload2.getBytes())); - - binding2 = binder.bindConsumer("defaultGroup.0", null, input2, - consumerProperties); - // Since we don't provide any topic info, let Kafka bind the consumer successfully - Thread.sleep(1000); - String testPayload3 = "foo-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload3.getBytes())); - - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload2); - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload(), StandardCharsets.UTF_8)) - .isNotNull(); - - receivedMessage2 = (Message) receive(input2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String(receivedMessage2.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload3); - - Map topicsInUse = ((KafkaTestBinder) binder) - .getCoreBinder().getTopicsInUse(); - assertThat(topicsInUse.keySet()).contains("defaultGroup.0"); - TopicInformation topic = topicsInUse.get("defaultGroup.0"); - assertThat(topic.isConsumerTopic()).isTrue(); - assertThat(topic.consumerGroup()).startsWith("anonymous"); - - producerBinding.unbind(); - binding1.unbind(); - binding2.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testPartitionedModuleJavaWithRawMode() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties properties = createProducerProperties(); - properties.setHeaderMode(HeaderMode.none); - this.applicationContext.registerBean("pkExtractor", - RawKafkaPartitionTestSupport.class, () -> new RawKafkaPartitionTestSupport()); - this.applicationContext.registerBean("pkSelector", - RawKafkaPartitionTestSupport.class, () -> new RawKafkaPartitionTestSupport()); - properties.setPartitionKeyExtractorName("pkExtractor"); - properties.setPartitionSelectorName("pkSelector"); - properties.setPartitionCount(6); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(properties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("partJ.raw.0", output, - properties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setConcurrency(2); - consumerProperties.setInstanceCount(3); - consumerProperties.setInstanceIndex(0); - consumerProperties.setPartitioned(true); - consumerProperties.setHeaderMode(HeaderMode.none); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.input0J"); - Binding input0Binding = binder.bindConsumer("partJ.raw.0", "test", - input0, consumerProperties); - consumerProperties.setInstanceIndex(1); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1J"); - Binding input1Binding = binder.bindConsumer("partJ.raw.0", "test", - input1, consumerProperties); - consumerProperties.setInstanceIndex(2); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2J"); - Binding input2Binding = binder.bindConsumer("partJ.raw.0", "test", - input2, consumerProperties); - - output.send(new GenericMessage<>(new byte[] { (byte) 0 })); - output.send(new GenericMessage<>(new byte[] { (byte) 1 })); - output.send(new GenericMessage<>(new byte[] { (byte) 2 })); - - Message receive0 = receive(input0); - assertThat(receive0).isNotNull(); - Message receive1 = receive(input1); - assertThat(receive1).isNotNull(); - Message receive2 = receive(input2); - assertThat(receive2).isNotNull(); - - assertThat(Arrays.asList(((byte[]) receive0.getPayload())[0], - ((byte[]) receive1.getPayload())[0], ((byte[]) receive2.getPayload())[0])) - .containsExactlyInAnyOrder((byte) 0, (byte) 1, (byte) 2); - - input0Binding.unbind(); - input1Binding.unbind(); - input2Binding.unbind(); - outputBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testPartitionedModuleSpELWithRawMode() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties properties = createProducerProperties(); - properties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("payload[0]")); - properties.setPartitionSelectorExpression( - spelExpressionParser.parseExpression("hashCode()")); - properties.setPartitionCount(6); - properties.setHeaderMode(HeaderMode.none); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(properties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("part.raw.0", output, - properties); - try { - Object endpoint = extractEndpoint(outputBinding); - assertThat(getEndpointRouting(endpoint)) - .contains(getExpectedRoutingBaseDestination("part.raw.0", "test") - + "-' + headers['partition']"); - } - catch (UnsupportedOperationException ignored) { - } - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setConcurrency(2); - consumerProperties.setInstanceIndex(0); - consumerProperties.setInstanceCount(3); - consumerProperties.setPartitioned(true); - consumerProperties.setHeaderMode(HeaderMode.none); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.input0S"); - Binding input0Binding = binder.bindConsumer("part.raw.0", "test", - input0, consumerProperties); - consumerProperties.setInstanceIndex(1); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1S"); - Binding input1Binding = binder.bindConsumer("part.raw.0", "test", - input1, consumerProperties); - consumerProperties.setInstanceIndex(2); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2S"); - Binding input2Binding = binder.bindConsumer("part.raw.0", "test", - input2, consumerProperties); - - Message message2 = org.springframework.integration.support.MessageBuilder - .withPayload(new byte[] { 2 }) - .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, - "kafkaBinderTestCommonsDelegate") - .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42) - .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 43).build(); - output.send(message2); - output.send(new GenericMessage<>(new byte[] { 1 })); - output.send(new GenericMessage<>(new byte[] { 0 })); - Message receive0 = receive(input0); - assertThat(receive0).isNotNull(); - Message receive1 = receive(input1); - assertThat(receive1).isNotNull(); - Message receive2 = receive(input2); - assertThat(receive2).isNotNull(); - assertThat(Arrays.asList(((byte[]) receive0.getPayload())[0], - ((byte[]) receive1.getPayload())[0], ((byte[]) receive2.getPayload())[0])) - .containsExactlyInAnyOrder((byte) 0, (byte) 1, (byte) 2); - input0Binding.unbind(); - input1Binding.unbind(); - input2Binding.unbind(); - outputBinding.unbind(); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testPartitionedNative() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties properties = createProducerProperties(); - properties.setPartitionCount(6); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(properties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("partNative.raw.0", - output, properties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.inputNative"); - Binding inputBinding = binder.bindConsumer("partNative.raw.0", - "test", input0, consumerProperties); - - output.send(new GenericMessage<>("foo".getBytes(), - Collections.singletonMap(KafkaHeaders.PARTITION, 5))); - - Message received = receive(input0); - assertThat(received).isNotNull(); - - assertThat(received.getPayload()).isEqualTo("foo".getBytes()); - assertThat(received.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION)) - .isEqualTo(5); - - inputBinding.unbind(); - outputBinding.unbind(); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testSendAndReceiveWithRawMode() throws Exception { - Binder binder = getBinder(); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setHeaderMode(HeaderMode.none); - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setHeaderMode(HeaderMode.none); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - Binding producerBinding = binder.bindProducer("raw.0", - moduleOutputChannel, producerProperties); - - Binding consumerBinding = binder.bindConsumer("raw.0", "test", - moduleInputChannel, consumerProperties); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("testSendAndReceiveWithRawMode".getBytes()).build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat( - new String(inboundMessageRef.get().getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("testSendAndReceiveWithRawMode"); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testAllowNonTransactionalProducerSetting() throws Exception { - AbstractKafkaTestBinder binder = getBinder(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = new ExtendedProducerProperties<>( - new KafkaProducerProperties()); - producerProps.getExtension().setAllowNonTransactional(true); - Binding producerBinding = binder.bindProducer("allwNonTrans.0", - moduleOutputChannel, producerProps); - - KafkaProducerMessageHandler endpoint = TestUtils.getPropertyValue(producerBinding, - "lifecycle", KafkaProducerMessageHandler.class); - - final KafkaTemplate kafkaTemplate = (KafkaTemplate) new DirectFieldAccessor(endpoint).getPropertyValue("kafkaTemplate"); - - assertThat(kafkaTemplate.isAllowNonTransactional()).isTrue(); - - producerBinding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testProducerErrorChannel() throws Exception { - AbstractKafkaTestBinder binder = getBinder(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = new ExtendedProducerProperties<>( - new KafkaProducerProperties()); - producerProps.setHeaderMode(HeaderMode.none); - producerProps.setErrorChannelEnabled(true); - producerProps.populateBindingName("foobar"); - Binding producerBinding = binder.bindProducer("ec.0", - moduleOutputChannel, producerProps); - final Message message = MessageBuilder.withPayload("bad") - .setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build(); - SubscribableChannel ec = binder.getApplicationContext().getBean(binder.getBinder().getBinderIdentity() + ".foobar.errors", SubscribableChannel.class); - final AtomicReference> errorMessage = new AtomicReference<>(); - final CountDownLatch latch = new CountDownLatch(2); - ec.subscribe(message1 -> { - errorMessage.set(message1); - latch.countDown(); - }); - SubscribableChannel globalEc = binder.getApplicationContext().getBean( - IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, - SubscribableChannel.class); - globalEc.subscribe(message12 -> latch.countDown()); - KafkaProducerMessageHandler endpoint = TestUtils.getPropertyValue(producerBinding, - "lifecycle", KafkaProducerMessageHandler.class); - final RuntimeException fooException = new RuntimeException("foo"); - final AtomicReference sent = new AtomicReference<>(); - new DirectFieldAccessor(endpoint).setPropertyValue("kafkaTemplate", - new KafkaTemplate(mock(ProducerFactory.class)) { - - @Override // SIK < 2.3 - public CompletableFuture send(String topic, Object payload) { - sent.set(payload); - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(fooException); - return future; - } - - @Override // SIK 2.3+ - public CompletableFuture send(ProducerRecord record) { - sent.set(record.value()); - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(fooException); - return future; - } - - }); - - moduleOutputChannel.send(message); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(errorMessage.get()).isInstanceOf(ErrorMessage.class); - assertThat(errorMessage.get().getPayload()) - .isInstanceOf(KafkaSendFailureException.class); - KafkaSendFailureException exception = (KafkaSendFailureException) errorMessage - .get().getPayload(); - assertThat(exception.getCause()).isSameAs(fooException); - assertThat(new String((byte[]) exception.getFailedMessage().getPayload(), - StandardCharsets.UTF_8)).isEqualTo(message.getPayload()); - assertThat(exception.getRecord().value()).isSameAs(sent.get()); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testAutoCreateTopicsEnabledSucceeds() throws Exception { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - configurationProperties.setAutoCreateTopics(true); - Binder binder = getBinder(configurationProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - String testTopicName = "nonexisting" + System.currentTimeMillis(); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding binding = binder.bindConsumer(testTopicName, "test", - moduleInputChannel, consumerProperties); - binding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testCustomPartitionCountOverridesDefaultIfLarger() throws Exception { - byte[] testPayload = new byte[2048]; - Arrays.fill(testPayload, (byte) 65); - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - binderConfiguration.setMinPartitionCount(10); - Binder binder = getBinder(binderConfiguration); - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setPartitionCount(10); - producerProperties.setPartitionKeyExpression(new LiteralExpression("foo")); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - Binding consumerBinding = binder.bindConsumer( - "foo" + uniqueBindingId + ".0", null, moduleInputChannel, - consumerProperties); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload).build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - Message inbound = receive(moduleInputChannel); - assertThat(inbound).isNotNull(); - assertThat((byte[]) inbound.getPayload()).containsExactly(testPayload); - - assertThat(partitionSize("foo" + uniqueBindingId + ".0")).isEqualTo(10); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testCustomPartitionCountDoesNotOverridePartitioningIfSmaller() - throws Exception { - byte[] testPayload = new byte[2048]; - Arrays.fill(testPayload, (byte) 65); - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - binderConfiguration.setMinPartitionCount(6); - Binder binder = getBinder(binderConfiguration); - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setPartitionCount(5); - producerProperties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("payload")); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - long uniqueBindingId = System.currentTimeMillis(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - Binding producerBinding = binder.bindProducer( - "foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - Binding consumerBinding = binder.bindConsumer( - "foo" + uniqueBindingId + ".0", null, moduleInputChannel, - consumerProperties); - Thread.sleep(1000); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload).build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - Message inbound = receive(moduleInputChannel); - assertThat(inbound).isNotNull(); - assertThat((byte[]) inbound.getPayload()).containsExactly(testPayload); - - assertThat(partitionSize("foo" + uniqueBindingId + ".0")).isEqualTo(6); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testDynamicKeyExpression() throws Exception { - Binder binder = getBinder(createConfigurationProperties()); - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension().getConfiguration().put("key.serializer", - StringSerializer.class.getName()); - producerProperties.getExtension().setMessageKeyExpression( - spelExpressionParser.parseExpression("headers.key")); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - String uniqueBindingId = UUID.randomUUID().toString(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - Binding producerBinding = binder.bindProducer( - "foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - Binding consumerBinding = binder.bindConsumer( - "foo" + uniqueBindingId + ".0", null, moduleInputChannel, - consumerProperties); - Thread.sleep(1000); - Message message = MessageBuilder.withPayload("somePayload") - .setHeader("key", "myDynamicKey").build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - Message inbound = receive(moduleInputChannel); - assertThat(inbound).isNotNull(); - String receivedKey = new String(inbound.getHeaders() - .get(KafkaHeaders.RECEIVED_KEY, byte[].class)); - assertThat(receivedKey).isEqualTo("myDynamicKey"); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testCustomPartitionCountOverridesPartitioningIfLarger() throws Exception { - byte[] testPayload = new byte[2048]; - Arrays.fill(testPayload, (byte) 65); - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - binderConfiguration.setMinPartitionCount(4); - Binder binder = getBinder(binderConfiguration); - - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setPartitionCount(5); - producerProperties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("payload")); - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - long uniqueBindingId = System.currentTimeMillis(); - Binding producerBinding = binder.bindProducer( - "foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - Binding consumerBinding = binder.bindConsumer( - "foo" + uniqueBindingId + ".0", null, moduleInputChannel, - consumerProperties); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload(testPayload).build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - Message inbound = receive(moduleInputChannel); - assertThat(inbound).isNotNull(); - assertThat((byte[]) inbound.getPayload()).containsExactly(testPayload); - assertThat(partitionSize("foo" + uniqueBindingId + ".0")).isEqualTo(5); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testDefaultConsumerStartsAtEarliest() throws Exception { - Binder binder = getBinder(createConfigurationProperties()); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel output = createBindableChannel("output", producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - - DirectChannel input1 = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - String testTopicName = UUID.randomUUID().toString(); - Binding producerBinding = binder.bindProducer(testTopicName, - output, createProducerProperties()); - String testPayload1 = "foo-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload1.getBytes())); - - Binding consumerBinding = binder.bindConsumer(testTopicName, - "startOffsets", input1, consumerProperties); - - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef1 = new AtomicReference<>(); - MessageHandler messageHandler = message1 -> { - try { - inboundMessageRef1.set((Message) message1); - } - finally { - latch.countDown(); - } - }; - input1.subscribe(messageHandler); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - assertThat(inboundMessageRef1.get()).isNotNull(); - assertThat( - new String(inboundMessageRef1.get().getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload1); - - String testPayload2 = "foo-" + UUID.randomUUID().toString(); - input1.unsubscribe(messageHandler); - output.send(new GenericMessage<>(testPayload2.getBytes())); - - CountDownLatch latch1 = new CountDownLatch(1); - AtomicReference> inboundMessageRef2 = new AtomicReference<>(); - input1.subscribe(message1 -> { - try { - inboundMessageRef2.set((Message) message1); - } - finally { - latch1.countDown(); - } - }); - Assert.isTrue(latch1.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef2.get()).isNotNull(); - assertThat( - new String(inboundMessageRef2.get().getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload2); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testResume() throws Exception { - Binding producerBinding = null; - Binding consumerBinding = null; - - try { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - Binder binder = getBinder(configurationProperties); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel output = createBindableChannel("output", - producerBindingProperties); - - DirectChannel input1 = createBindableChannel("input", - createConsumerBindingProperties(createConsumerProperties())); - - String testTopicName = UUID.randomUUID().toString(); - producerBinding = binder.bindProducer(testTopicName, output, - producerBindingProperties.getProducer()); - ExtendedConsumerProperties firstConsumerProperties = createConsumerProperties(); - consumerBinding = binder.bindConsumer(testTopicName, "startOffsets", input1, - firstConsumerProperties); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef1 = new AtomicReference<>(); - MessageHandler messageHandler = message1 -> { - try { - inboundMessageRef1.set((Message) message1); - } - finally { - latch.countDown(); - } - }; - input1.subscribe(messageHandler); - String testPayload1 = "foo1-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload1)); - Assert.isTrue(latch.await(15, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef1.get()).isNotNull(); - assertThat(inboundMessageRef1.get().getPayload()).isNotNull(); - input1.unsubscribe(messageHandler); - CountDownLatch latch1 = new CountDownLatch(1); - AtomicReference> inboundMessageRef2 = new AtomicReference<>(); - MessageHandler messageHandler1 = message1 -> { - try { - inboundMessageRef2.set((Message) message1); - } - finally { - latch1.countDown(); - } - }; - input1.subscribe(messageHandler1); - String testPayload2 = "foo2-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload2.getBytes())); - Assert.isTrue(latch1.await(15, TimeUnit.SECONDS), - "Failed to receive message"); - assertThat(inboundMessageRef2.get()).isNotNull(); - assertThat(inboundMessageRef2.get().getPayload()).isNotNull(); - consumerBinding.unbind(); - - Thread.sleep(2000); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerBinding = binder.bindConsumer(testTopicName, "startOffsets", input1, - consumerProperties); - input1.unsubscribe(messageHandler1); - CountDownLatch latch2 = new CountDownLatch(1); - AtomicReference> inboundMessageRef3 = new AtomicReference<>(); - MessageHandler messageHandler2 = message1 -> { - try { - inboundMessageRef3.set((Message) message1); - } - finally { - latch2.countDown(); - } - }; - input1.subscribe(messageHandler2); - String testPayload3 = "foo3-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload3.getBytes())); - Assert.isTrue(latch2.await(15, TimeUnit.SECONDS), - "Failed to receive message"); - assertThat(inboundMessageRef3.get()).isNotNull(); - assertThat(new String(inboundMessageRef3.get().getPayload(), - StandardCharsets.UTF_8)).isEqualTo(testPayload3); - } - finally { - if (consumerBinding != null) { - consumerBinding.unbind(); - } - if (producerBinding != null) { - producerBinding.unbind(); - } - } - } - - @Test - @SuppressWarnings("unchecked") - void testSyncProducerMetadata() throws Exception { - Binder binder = getBinder(createConfigurationProperties()); - DirectChannel output = new DirectChannel(); - String testTopicName = UUID.randomUUID().toString(); - ExtendedProducerProperties properties = createProducerProperties(); - properties.getExtension().setSync(true); - Binding producerBinding = binder.bindProducer(testTopicName, - output, properties); - DirectFieldAccessor accessor = new DirectFieldAccessor( - extractEndpoint(producerBinding)); - KafkaProducerMessageHandler wrappedInstance = (KafkaProducerMessageHandler) accessor - .getWrappedInstance(); - assertThat(new DirectFieldAccessor(wrappedInstance).getPropertyValue("sync") - .equals(Boolean.TRUE)) - .withFailMessage("Kafka Sync Producer should have been enabled."); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testSendTimeoutExpressionProducerMetadata() throws Exception { - Binder binder = getBinder(createConfigurationProperties()); - DirectChannel output = new DirectChannel(); - String testTopicName = UUID.randomUUID().toString(); - ExtendedProducerProperties properties = createProducerProperties(); - properties.getExtension().setSync(true); - SpelExpressionParser parser = new SpelExpressionParser(); - Expression sendTimeoutExpression = parser.parseExpression("5000"); - properties.getExtension().setSendTimeoutExpression(sendTimeoutExpression); - Binding producerBinding = binder.bindProducer(testTopicName, - output, properties); - DirectFieldAccessor accessor = new DirectFieldAccessor( - extractEndpoint(producerBinding)); - KafkaProducerMessageHandler wrappedInstance = (KafkaProducerMessageHandler) accessor - .getWrappedInstance(); - assertThat(new DirectFieldAccessor(wrappedInstance).getPropertyValue("sendTimeoutExpression") - .equals(sendTimeoutExpression)); - producerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testAutoCreateTopicsDisabledOnBinderStillWorksAsLongAsBrokerCreatesTopic() - throws Exception { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - configurationProperties.setAutoCreateTopics(false); - Binder binder = getBinder(configurationProperties); - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties()); - DirectChannel output = createBindableChannel("output", producerBindingProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - String testTopicName = "createdByBroker-" + System.currentTimeMillis(); - - Binding producerBinding = binder.bindProducer(testTopicName, - output, producerBindingProperties.getProducer()); - - String testPayload = "foo1-" + UUID.randomUUID().toString(); - output.send(new GenericMessage<>(testPayload)); - - Binding consumerBinding = binder.bindConsumer(testTopicName, - "test", input, consumerProperties); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - input.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat( - new String(inboundMessageRef.get().getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(testPayload); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testAutoConfigureTopicsDisabledSucceedsIfTopicExisting() - throws Throwable { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - String testTopicName = "existing" + System.currentTimeMillis(); - invokeCreateTopic(testTopicName, 5, 1); - configurationProperties.setAutoCreateTopics(false); - Binder binder = getBinder(configurationProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - Binding binding = binder.bindConsumer(testTopicName, "test", - input, consumerProperties); - binding.unbind(); - } - - @Test - @SuppressWarnings("unchecked") - void testPartitionCountIncreasedIfAutoAddPartitionsSet() throws Throwable { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - String testTopicName = "existing" + System.currentTimeMillis(); - configurationProperties.setMinPartitionCount(6); - configurationProperties.setAutoAddPartitions(true); - Binder binder = getBinder(configurationProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding binding = binder.bindConsumer(testTopicName, "test", input, - consumerProperties); - binding.unbind(); - assertThat(invokePartitionSize(testTopicName)).isEqualTo(6); - } - - @Test - @SuppressWarnings("unchecked") - void testAutoAddPartitionsDisabledSucceedsIfTopicUnderPartitionedAndAutoRebalanceEnabled() - throws Throwable { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - String testTopicName = "existing" + System.currentTimeMillis(); - invokeCreateTopic(testTopicName, 1, 1); - configurationProperties.setAutoAddPartitions(false); - Binder binder = getBinder(configurationProperties); - GenericApplicationContext context = new GenericApplicationContext(); - context.refresh(); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - // this consumer must consume from partition 2 - consumerProperties.setInstanceCount(3); - consumerProperties.setInstanceIndex(2); - Binding binding = binder.bindConsumer(testTopicName, "test", input, - consumerProperties); - binding.unbind(); - assertThat(invokePartitionSize(testTopicName)).isEqualTo(1); - } - - @Test - @SuppressWarnings("unchecked") - void testAutoAddPartitionsDisabledFailsIfTopicUnderPartitionedAndAutoRebalanceDisabled() - throws Throwable { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - String testTopicName = "existing" + System.currentTimeMillis(); - invokeCreateTopic(testTopicName, 1, 1); - configurationProperties.setAutoAddPartitions(false); - Binder binder = getBinder(configurationProperties); - GenericApplicationContext context = new GenericApplicationContext(); - context.refresh(); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel output = createBindableChannel("output", - createConsumerBindingProperties(consumerProperties)); - // this consumer must consume from partition 2 - consumerProperties.setInstanceCount(3); - consumerProperties.setInstanceIndex(2); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - Assertions.assertThatThrownBy(() -> { - Binding binding = binder.bindConsumer(testTopicName, "test", output, - consumerProperties); - if (binding != null) { - binding.unbind(); - } - }).isInstanceOf(ProvisioningException.class); - } - - @Test - @SuppressWarnings("unchecked") - void testAutoAddPartitionsDisabledSucceedsIfTopicPartitionedCorrectly() - throws Throwable { - Binding binding = null; - try { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - String testTopicName = "existing" + System.currentTimeMillis(); - invokeCreateTopic(testTopicName, 6, 1); - configurationProperties.setAutoAddPartitions(false); - Binder binder = getBinder(configurationProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - // this consumer must consume from partition 2 - consumerProperties.setInstanceCount(3); - consumerProperties.setInstanceIndex(2); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - - binding = binder.bindConsumer(testTopicName, "test-x", input, - consumerProperties); - - ContainerProperties containerProps = TestUtils.getPropertyValue( - binding, - "lifecycle.messageListenerContainer.containerProperties", - ContainerProperties.class); - TopicPartitionOffset[] listenedPartitions = containerProps.getTopicPartitions(); - assertThat(listenedPartitions).hasSize(2); - assertThat(listenedPartitions).contains( - new TopicPartitionOffset(testTopicName, 2), - new TopicPartitionOffset(testTopicName, 5)); - int partitions = invokePartitionSize(testTopicName); - assertThat(partitions).isEqualTo(6); - } - finally { - if (binding != null) { - binding.unbind(); - } - } - } - - @Test - @SuppressWarnings("unchecked") - void testPartitionCountNotReduced() throws Throwable { - String testTopicName = "existing" + System.currentTimeMillis(); - - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - - invokeCreateTopic(testTopicName, 6, 1); - configurationProperties.setAutoAddPartitions(true); - Binder binder = getBinder(configurationProperties); - GenericApplicationContext context = new GenericApplicationContext(); - context.refresh(); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding binding = binder.bindConsumer(testTopicName, "test", input, - consumerProperties); - binding.unbind(); - - assertThat(partitionSize(testTopicName)).isEqualTo(6); - } - - @Test - @SuppressWarnings("unchecked") - void testConsumerDefaultDeserializer() throws Throwable { - Binding binding = null; - try { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - String testTopicName = "existing" + System.currentTimeMillis(); - invokeCreateTopic(testTopicName, 5, 1); - configurationProperties.setAutoCreateTopics(false); - Binder binder = getBinder(configurationProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - binding = binder.bindConsumer(testTopicName, "test", input, - consumerProperties); - DirectFieldAccessor consumerAccessor = new DirectFieldAccessor( - getKafkaConsumer(binding)); - assertThat(consumerAccessor - .getPropertyValue("keyDeserializer") instanceof ByteArrayDeserializer) - .isTrue(); - assertThat(consumerAccessor.getPropertyValue( - "valueDeserializer") instanceof ByteArrayDeserializer).isTrue(); - } - finally { - if (binding != null) { - binding.unbind(); - } - } - } - - @Test - @SuppressWarnings("unchecked") - void testConsumerCustomDeserializer() throws Exception { - Binding binding = null; - try { - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - Map propertiesToOverride = configurationProperties - .getConfiguration(); - propertiesToOverride.put("key.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - propertiesToOverride.put("value.deserializer", - "org.apache.kafka.common.serialization.LongDeserializer"); - configurationProperties.setConfiguration(propertiesToOverride); - String testTopicName = "existing" + System.currentTimeMillis(); - configurationProperties.setAutoCreateTopics(false); - Binder binder = getBinder(configurationProperties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel input = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - binding = binder.bindConsumer(testTopicName, "test", input, - consumerProperties); - DirectFieldAccessor consumerAccessor = new DirectFieldAccessor( - getKafkaConsumer(binding)); - assertThat(consumerAccessor - .getPropertyValue("keyDeserializer") instanceof StringDeserializer) - .isTrue(); - assertThat(consumerAccessor - .getPropertyValue("valueDeserializer") instanceof LongDeserializer) - .isTrue(); - } - finally { - if (binding != null) { - binding.unbind(); - } - } - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testNativeSerializationWithCustomSerializerDeserializer() - throws Exception { - Binding producerBinding = null; - Binding consumerBinding = null; - try { - Integer testPayload = 10; - Message message = MessageBuilder.withPayload(testPayload).build(); - SubscribableChannel moduleOutputChannel = new DirectChannel(); - String testTopicName = "existing" + System.currentTimeMillis(); - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - configurationProperties.setAutoAddPartitions(true); - Binder binder = getBinder(configurationProperties); - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setUseNativeEncoding(true); - producerProperties.getExtension().getConfiguration().put("value.serializer", - "org.apache.kafka.common.serialization.IntegerSerializer"); - producerBinding = binder.bindProducer(testTopicName, moduleOutputChannel, - producerProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - consumerProperties.getExtension().getConfiguration().put("value.deserializer", - "org.apache.kafka.common.serialization.IntegerDeserializer"); - consumerProperties.getExtension() - .setStandardHeaders(KafkaConsumerProperties.StandardHeaders.both); - consumerBinding = binder.bindConsumer(testTopicName, "test", - moduleInputChannel, consumerProperties); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - Message inbound = receive(moduleInputChannel, 500); - assertThat(inbound).isNotNull(); - assertThat(inbound.getPayload()).isEqualTo(10); - assertThat(inbound.getHeaders()).doesNotContainKey("contentType"); - assertThat(inbound.getHeaders().getId()).isNotNull(); - assertThat(inbound.getHeaders().getTimestamp()).isNotNull(); - } - finally { - if (producerBinding != null) { - producerBinding.unbind(); - } - if (consumerBinding != null) { - consumerBinding.unbind(); - } - } - } - - private KafkaConsumer getKafkaConsumer(Binding binding) { - DirectFieldAccessor bindingAccessor = new DirectFieldAccessor(binding); - KafkaMessageDrivenChannelAdapter adapter = (KafkaMessageDrivenChannelAdapter) bindingAccessor - .getPropertyValue("lifecycle"); - DirectFieldAccessor adapterAccessor = new DirectFieldAccessor(adapter); - ConcurrentMessageListenerContainer messageListenerContainer = (ConcurrentMessageListenerContainer) adapterAccessor - .getPropertyValue("messageListenerContainer"); - DirectFieldAccessor containerAccessor = new DirectFieldAccessor( - messageListenerContainer); - DefaultKafkaConsumerFactory consumerFactory = (DefaultKafkaConsumerFactory) containerAccessor - .getPropertyValue("consumerFactory"); - return (KafkaConsumer) consumerFactory.createConsumer(); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testNativeSerializationWithCustomSerializerDeserializerBytesPayload() - throws Exception { - Binding producerBinding = null; - Binding consumerBinding = null; - try { - byte[] testPayload = new byte[1]; - Message message = MessageBuilder.withPayload(testPayload) - .setHeader(MessageHeaders.CONTENT_TYPE, "something/funky").build(); - SubscribableChannel moduleOutputChannel = new DirectChannel(); - String testTopicName = "existing" + System.currentTimeMillis(); - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - configurationProperties.setAutoAddPartitions(true); - Binder binder = getBinder(configurationProperties); - ConfigurableApplicationContext context = TestUtils.getPropertyValue(binder, - "binder.applicationContext", ConfigurableApplicationContext.class); - MessagingMessageConverter converter = new MessagingMessageConverter(); - converter.setGenerateMessageId(true); - converter.setGenerateTimestamp(true); - context.getBeanFactory().registerSingleton("testConverter", converter); - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.setUseNativeEncoding(true); - producerProperties.getExtension().getConfiguration().put( - ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, - ByteArraySerializer.class.getName()); - producerBinding = binder.bindProducer(testTopicName, moduleOutputChannel, - producerProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - consumerProperties.getExtension().getConfiguration().put( - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class.getName()); - consumerProperties.getExtension().setConverterBeanName("testConverter"); - consumerBinding = binder.bindConsumer(testTopicName, "test", - moduleInputChannel, consumerProperties); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - Message inbound = receive(moduleInputChannel, 500); - assertThat(inbound).isNotNull(); - assertThat(inbound.getPayload()).isEqualTo(new byte[1]); - assertThat(inbound.getHeaders()).containsKey("contentType"); - assertThat(inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString()) - .isEqualTo("something/funky"); - assertThat(inbound.getHeaders().getId()).isNotNull(); - assertThat(inbound.getHeaders().getTimestamp()).isNotNull(); - } - finally { - if (producerBinding != null) { - producerBinding.unbind(); - } - if (consumerBinding != null) { - consumerBinding.unbind(); - } - } - } - - @Test - @SuppressWarnings("unchecked") - void testBuiltinSerialization() throws Exception { - Binding producerBinding = null; - Binding consumerBinding = null; - try { - String testPayload = "test"; - Message message = MessageBuilder.withPayload(testPayload) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build(); - - ExtendedProducerProperties producerProperties = createProducerProperties(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setAutoRebalanceEnabled(false); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - String testTopicName = "existing" + System.currentTimeMillis(); - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - configurationProperties.setAutoAddPartitions(true); - Binder binder = getBinder(configurationProperties); - producerBinding = binder.bindProducer(testTopicName, moduleOutputChannel, - producerProperties); - - consumerBinding = binder.bindConsumer(testTopicName, "test", - moduleInputChannel, consumerProperties); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat(inboundMessageRef.get().getPayload()).isEqualTo("test".getBytes()); - assertThat(inboundMessageRef.get().getHeaders()).containsEntry("contentType", - MimeTypeUtils.TEXT_PLAIN); - } - finally { - if (producerBinding != null) { - producerBinding.unbind(); - } - if (consumerBinding != null) { - consumerBinding.unbind(); - } - } - } - - /* - * Verify that a consumer configured to handle embedded headers can handle all three - * variants. - */ - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - void testSendAndReceiveWithMixedMode() throws Exception { - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - binderConfiguration.setHeaders("foo"); - Binder binder = getBinder(binderConfiguration); - DirectChannel moduleOutputChannel1 = new DirectChannel(); - ExtendedProducerProperties producerProperties1 = createProducerProperties(); - producerProperties1.setHeaderMode(HeaderMode.embeddedHeaders); - Binding producerBinding1 = binder.bindProducer("mixed.0", - moduleOutputChannel1, producerProperties1); - - DirectChannel moduleOutputChannel2 = new DirectChannel(); - ExtendedProducerProperties producerProperties2 = createProducerProperties(); - producerProperties2.setHeaderMode(HeaderMode.headers); - Binding producerBinding2 = binder.bindProducer("mixed.0", - moduleOutputChannel2, producerProperties2); - - DirectChannel moduleOutputChannel3 = new DirectChannel(); - ExtendedProducerProperties producerProperties3 = createProducerProperties(); - producerProperties3.setHeaderMode(HeaderMode.none); - Binding producerBinding3 = binder.bindProducer("mixed.0", - moduleOutputChannel3, producerProperties3); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setHeaderMode(HeaderMode.embeddedHeaders); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - QueueChannel bridged = new QueueChannel(); - BridgeHandler bridge = new BridgeHandler(); - bridge.setOutputChannel(bridged); - moduleInputChannel.subscribe(bridge); - Binding consumerBinding = binder.bindConsumer("mixed.0", "test", - moduleInputChannel, consumerProperties); - Message message = org.springframework.integration.support.MessageBuilder - .withPayload("testSendAndReceiveWithMixedMode".getBytes()) - .setHeader("foo", "bar").build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel1.send(message); - moduleOutputChannel2.send(message); - moduleOutputChannel3.send(message); - Message inbound = receive(bridged, 10_000); - assertThat(inbound).isNotNull(); - assertThat(new String((byte[]) inbound.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("testSendAndReceiveWithMixedMode"); - assertThat(inbound.getHeaders().get("foo")).isEqualTo("bar"); - assertThat(inbound.getHeaders().get(BinderHeaders.NATIVE_HEADERS_PRESENT)) - .isNull(); - inbound = receive(bridged); - assertThat(inbound).isNotNull(); - assertThat(new String((byte[]) inbound.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("testSendAndReceiveWithMixedMode"); - assertThat(inbound.getHeaders().get("foo")).isEqualTo("bar"); - assertThat(inbound.getHeaders().get(BinderHeaders.NATIVE_HEADERS_PRESENT)) - .isEqualTo(Boolean.TRUE); - inbound = receive(bridged); - assertThat(inbound).isNotNull(); - assertThat(new String((byte[]) inbound.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("testSendAndReceiveWithMixedMode"); - assertThat(inbound.getHeaders().get("foo")).isNull(); - assertThat(inbound.getHeaders().get(BinderHeaders.NATIVE_HEADERS_PRESENT)) - .isNull(); - - Map consumerProps = KafkaTestUtils.consumerProps( - "testSendAndReceiveWithMixedMode", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - ByteArrayDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - Consumer consumer = cf.createConsumer(); - consumer.subscribe(Collections.singletonList("mixed.0")); - - ConsumerRecords records = consumer.poll(10_1000); - Iterator iterator = records.iterator(); - ConsumerRecord record = iterator.next(); - byte[] value = (byte[]) record.value(); - assertThat(value[0] & 0xff).isEqualTo(0xff); - assertThat(record.headers().toArray().length).isEqualTo(0); - record = iterator.next(); - value = (byte[]) record.value(); - assertThat(value[0] & 0xff).isNotEqualTo(0xff); - assertThat(record.headers().toArray().length).isEqualTo(2); - record = iterator.next(); - value = (byte[]) record.value(); - assertThat(value[0] & 0xff).isNotEqualTo(0xff); - assertThat(record.headers().toArray().length).isEqualTo(0); - consumer.close(); - - producerBinding1.unbind(); - producerBinding2.unbind(); - producerBinding3.unbind(); - consumerBinding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testPolledConsumer() throws Exception { - KafkaTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties consumerProps = createConsumerProperties(); - consumerProps.setMultiplex(true); - consumerProps.getExtension().setPollTimeout(1); - Binding> binding = binder.bindPollableConsumer( - "pollable,anotherOne", "group-polledConsumer", inboundBindTarget, - consumerProps); - Map producerProps = KafkaTestUtils - .producerProps(embeddedKafka); - KafkaTemplate template = new KafkaTemplate( - new DefaultKafkaProducerFactory<>(producerProps)); - template.send("pollable", "testPollable"); - boolean polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable"); - }); - int n = 0; - while (n++ < 100 && !polled) { - polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable".getBytes()); - }); - Thread.sleep(100); - } - assertThat(polled).isTrue(); - - template.send("anotherOne", "testPollable2"); - polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable2"); - }); - n = 0; - while (n++ < 100 && !polled) { - polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable2".getBytes()); - }); - Thread.sleep(100); - } - assertThat(polled).isTrue(); - // Bind a second pollable consumer GH-521 - consumerProps.getExtension().getConfiguration() - .put(ConsumerConfig.CLIENT_ID_CONFIG, "pollable2"); - PollableSource second = new DefaultPollableMessageSource( - this.messageConverter); - Binding> binding2 = binder.bindPollableConsumer( - "pollable2", "group-polledConsumer2", second, consumerProps); - second.poll(m -> { - }); - binding.unbind(); - binding2.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testPolledConsumerRequeue() throws Exception { - KafkaTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties properties = createConsumerProperties(); - Binding> binding = binder.bindPollableConsumer( - "pollableRequeue", "group", inboundBindTarget, properties); - Map producerProps = KafkaTestUtils - .producerProps(embeddedKafka); - KafkaTemplate template = new KafkaTemplate( - new DefaultKafkaProducerFactory<>(producerProps)); - template.send("pollableRequeue", "testPollable"); - try { - boolean polled = false; - int n = 0; - while (n++ < 100 && !polled) { - polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable".getBytes()); - throw new RequeueCurrentMessageException(); - }); - } - } - catch (MessageHandlingException e) { - assertThat(e.getCause()).isInstanceOf(RequeueCurrentMessageException.class); - } - boolean polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable".getBytes()); - }); - assertThat(polled).isTrue(); - binding.unbind(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testPolledConsumerWithDlq() throws Exception { - KafkaTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setPollTimeout(1); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - properties.getExtension().setEnableDlq(true); - Map producerProps = KafkaTestUtils - .producerProps(embeddedKafka); - Binding> binding = binder.bindPollableConsumer( - "pollableDlq", "group-pcWithDlq", inboundBindTarget, properties); - KafkaTemplate template = new KafkaTemplate( - new DefaultKafkaProducerFactory<>(producerProps)); - template.send("pollableDlq", "testPollableDLQ"); - try { - int n = 0; - while (n++ < 100) { - inboundBindTarget.poll(m -> { - throw new RuntimeException("test DLQ"); - }); - Thread.sleep(100); - } - } - catch (MessageHandlingException e) { - assertThat(e.getCause().getMessage()).isEqualTo("test DLQ"); - } - Map consumerProps = KafkaTestUtils.consumerProps("dlq", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - ConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - Consumer consumer = cf.createConsumer(); - embeddedKafka.consumeFromAnEmbeddedTopic(consumer, - "error.pollableDlq.group-pcWithDlq"); - ConsumerRecord deadLetter = KafkaTestUtils.getSingleRecord(consumer, - "error.pollableDlq.group-pcWithDlq"); - assertThat(deadLetter).isNotNull(); - assertThat(deadLetter.value()).isEqualTo("testPollableDLQ"); - binding.unbind(); - consumer.close(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testTopicPatterns() throws Exception { - try (AdminClient admin = AdminClient.create( - Collections.singletonMap(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, - embeddedKafka.getBrokersAsString()))) { - admin.createTopics(Collections - .singletonList(new NewTopic("topicPatterns.1", 1, (short) 1))).all() - .get(); - Binder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setDestinationIsPattern(true); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference topic = new AtomicReference<>(); - moduleInputChannel.subscribe(m -> { - topic.set(m.getHeaders().get(KafkaHeaders.RECEIVED_TOPIC, String.class)); - latch.countDown(); - }); - Binding consumerBinding = binder.bindConsumer( - "topicPatterns\\..*", "testTopicPatterns", moduleInputChannel, - consumerProperties); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory( - KafkaTestUtils.producerProps(embeddedKafka)); - KafkaTemplate template = new KafkaTemplate(pf); - template.send("topicPatterns.1", "foo"); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(topic.get()).isEqualTo("topicPatterns.1"); - consumerBinding.unbind(); - pf.destroy(); - } - } - - @Test - void testSameTopicCannotBeProvisionedAgain() throws Throwable { - CountDownLatch latch = new CountDownLatch(1); - try (AdminClient admin = AdminClient.create( - Collections.singletonMap(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, - embeddedKafka.getBrokersAsString()))) { - admin.createTopics(Collections - .singletonList(new NewTopic("fooUniqueTopic", 1, (short) 1))).all() - .get(); - try { - admin.createTopics(Collections - .singletonList(new NewTopic("fooUniqueTopic", 1, (short) 1))) - .all().get(); - fail("Expecting TopicExistsException"); - } - catch (Exception ex) { - assertThat(ex.getCause() instanceof TopicExistsException).isTrue(); - latch.countDown(); - } - latch.await(1, TimeUnit.SECONDS); - } - } - - @Test - @SuppressWarnings("unchecked") - void testResetOffsets() throws Exception { - Binding producerBinding = null; - Binding consumerBinding = null; - try { - String testPayload = "test"; - - ExtendedProducerProperties producerProperties = createProducerProperties(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setConcurrency(2); - consumerProperties.setInstanceCount(5); // 10 partitions across 2 threads - consumerProperties.getExtension().setResetOffsets(true); - - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - String testTopicName = "existing" + System.currentTimeMillis(); - KafkaBinderConfigurationProperties configurationProperties = createConfigurationProperties(); - configurationProperties.setAutoAddPartitions(true); - Binder binder = getBinder(configurationProperties); - producerBinding = binder.bindProducer(testTopicName, moduleOutputChannel, - producerProperties); - - consumerBinding = binder.bindConsumer(testTopicName, "testReset", - moduleInputChannel, consumerProperties); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - IntStream.range(0, 10).forEach(i -> moduleOutputChannel.send(MessageBuilder.withPayload(testPayload) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .setHeader(KafkaHeaders.PARTITION, i) - .build())); - CountDownLatch latch1 = new CountDownLatch(10); - CountDownLatch latch2 = new CountDownLatch(20); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - AtomicInteger received = new AtomicInteger(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - received.incrementAndGet(); - latch1.countDown(); - latch2.countDown(); - } - }); - assertThat(latch1.await(10, TimeUnit.SECONDS)).as("Failed to receive messages").isTrue(); - consumerBinding.unbind(); - consumerBinding = binder.bindConsumer(testTopicName, "testReset", - moduleInputChannel, consumerProperties); - assertThat(latch2.await(10, TimeUnit.SECONDS)).as("Failed to receive message").isTrue(); - binder.bindConsumer(testTopicName + "-x", "testReset", - moduleInputChannel, consumerProperties).unbind(); // cause another rebalance - assertThat(received.get()).as("Unexpected reset").isEqualTo(20); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat(inboundMessageRef.get().getPayload()).isEqualTo("test".getBytes()); - assertThat(inboundMessageRef.get().getHeaders()).containsEntry("contentType", - MimeTypeUtils.TEXT_PLAIN); - } - finally { - if (producerBinding != null) { - producerBinding.unbind(); - } - if (consumerBinding != null) { - consumerBinding.unbind(); - } - } - } - - @Test - @SuppressWarnings("unchecked") - void testRecordMetadata() throws Exception { - Binding producerBinding = null; - try { - String testPayload = "test"; - - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension().setRecordMetadataChannel("metaChannel"); - QueueChannel metaChannel = new QueueChannel(); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - String testTopicName = "existing" + System.currentTimeMillis(); - KafkaTestBinder binder = getBinder(); - ((GenericApplicationContext) binder.getApplicationContext()).registerBean("metaChannel", - MessageChannel.class, () -> metaChannel); - producerBinding = binder.bindProducer(testTopicName, moduleOutputChannel, - producerProperties); - moduleOutputChannel - .send(new GenericMessage<>("foo", Collections.singletonMap(KafkaHeaders.PARTITION, 0))); - Message sendResult = metaChannel.receive(10_000); - assertThat(sendResult).isNotNull(); - RecordMetadata meta = sendResult.getHeaders().get(KafkaHeaders.RECORD_METADATA, RecordMetadata.class); - assertThat(meta).isNotNull() - .hasFieldOrPropertyWithValue("offset", 0L); - assertThat(meta.topic()).isEqualTo(testTopicName); - assertThat(meta.partition()).isEqualTo(0); - } - finally { - if (producerBinding != null) { - producerBinding.unbind(); - } - } - } - - @Test - @SuppressWarnings("unchecked") - void testMessageKeyInPayload() throws Exception { - Binding producerBinding = null; - try { - String testPayload = "test"; - - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension() - .setMessageKeyExpression(spelExpressionParser.parseExpression("payload.field.bytes")); - DirectChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - String testTopicName = "existing" + System.currentTimeMillis(); - KafkaTestBinder binder = getBinder(); - producerBinding = binder.bindProducer(testTopicName, moduleOutputChannel, - producerProperties); - moduleOutputChannel.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - assertThat(message.getHeaders() - .get(KafkaExpressionEvaluatingInterceptor.MESSAGE_KEY_HEADER)) - .isEqualTo("foo".getBytes()); - return message; - } - - }); - moduleOutputChannel.send( - new GenericMessage<>(new Pojo("foo"), Collections.singletonMap(KafkaHeaders.PARTITION, 0))); - } - finally { - if (producerBinding != null) { - producerBinding.unbind(); - } - } - } - - @Test - void testInternalHeadersNotPropagated() throws Exception { - testInternalHeadersNotPropagatedGuts("propagate.1", null, null); - } - - @Test - void testInternalHeadersNotPropagatedCustomHeader() throws Exception { - testInternalHeadersNotPropagatedGuts("propagate.2", new String[] { "foo", "*" }, null); - } - - @Test - void testInternalHeadersNotPropagatedCustomMapper() throws Exception { - testInternalHeadersNotPropagatedGuts("propagate.3", null, new BinderHeaderMapper("*")); - } - - public void testInternalHeadersNotPropagatedGuts(String name, String[] headerPatterns, - KafkaHeaderMapper mapper) throws Exception { - - KafkaTestBinder binder; - if (mapper == null) { - binder = getBinder(); - } - else { - KafkaBinderConfigurationProperties binderConfiguration = createConfigurationProperties(); - binderConfiguration.setHeaderMapperBeanName("headerMapper"); - - KafkaTopicProvisioner kafkaTopicProvisioner = new KafkaTopicProvisioner( - binderConfiguration, new TestKafkaProperties(), prop -> { - }); - try { - kafkaTopicProvisioner.afterPropertiesSet(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - binder = new KafkaTestBinder(binderConfiguration, kafkaTopicProvisioner); - ((GenericApplicationContext) binder.getApplicationContext()).registerBean("headerMapper", - KafkaHeaderMapper.class, () -> mapper); - } - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension().setHeaderPatterns(headerPatterns); - - DirectChannel output = createBindableChannel("output", createProducerBindingProperties(producerProperties)); - output.setBeanName(name + ".out"); - Binding producerBinding = binder.bindProducer(name + ".1", output, producerProperties); - - QueueChannel input = new QueueChannel(); - input.setBeanName(name + ".in"); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - Binding consumerBinding = binder.bindConsumer(name + ".0", name, input, consumerProperties); - Map producerProps = KafkaTestUtils.producerProps(embeddedKafka); - KafkaTemplate template = new KafkaTemplate(new DefaultKafkaProducerFactory<>(producerProps)); - template.send(MessageBuilder.withPayload("internalHeaderPropagation") - .setHeader(KafkaHeaders.TOPIC, name + ".0") - .setHeader("someHeader", "someValue") - .build()); - - Message consumed = input.receive(10_000); - if (headerPatterns != null) { - consumed = MessageBuilder.fromMessage(consumed).setHeader(headerPatterns[0], "bar").build(); - } - output.send(consumed); - - Map consumerProps = KafkaTestUtils.consumerProps(name, "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - Consumer consumer = cf.createConsumer(); - consumer.assign(Collections.singletonList(new TopicPartition(name + ".1", 0))); - ConsumerRecords records = consumer.poll(Duration.ofSeconds(10)); - assertThat(records.count()).isEqualTo(1); - ConsumerRecord received = records.iterator().next(); - assertThat(received.value()).isEqualTo("internalHeaderPropagation".getBytes()); - Header header = received.headers().lastHeader(BinderHeaders.NATIVE_HEADERS_PRESENT); - assertThat(header).isNull(); - header = received.headers().lastHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); - assertThat(header).isNull(); - header = received.headers().lastHeader(MessageHeaders.ID); - assertThat(header).isNull(); - header = received.headers().lastHeader(MessageHeaders.TIMESTAMP); - assertThat(header).isNull(); - assertThat(received.headers().lastHeader("someHeader")).isNotNull(); - if (headerPatterns != null) { - assertThat(received.headers().lastHeader(headerPatterns[0])).isNotNull(); - } - - producerBinding.unbind(); - consumerBinding.unbind(); - consumer.close(); - } - - @Test - void testNoBrokerOverride() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(); - producerProperties.getExtension().getConfiguration().put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "foo"); - BindingProperties outputBindingProperties = createProducerBindingProperties(producerProperties); - DirectChannel moduleOutputChannel = createBindableChannel("output", outputBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().getConfiguration().put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "foo"); - BindingProperties consumerBindingProperties = createConsumerBindingProperties(consumerProperties); - DirectChannel moduleInputChannel = createBindableChannel("input", consumerBindingProperties); - - assertThatExceptionOfType(BinderException.class).isThrownBy(() -> binder.bindProducer("foo.bar", - moduleOutputChannel, outputBindingProperties.getProducer())) - .withCauseExactlyInstanceOf(IllegalStateException.class); - assertThatExceptionOfType(BinderException.class).isThrownBy(() -> binder.bindConsumer("foo.bar", - "testSendAndReceive", moduleInputChannel, consumerProperties)) - .withCauseExactlyInstanceOf(IllegalStateException.class); - } - - @Test - void testObservationEnabledOnTheBinder() throws Exception { - KafkaBinderConfigurationProperties kafkaBinderConfigurationProperties = createConfigurationProperties(); - kafkaBinderConfigurationProperties.setEnableObservation(true); - AbstractKafkaTestBinder binder = getBinder(kafkaBinderConfigurationProperties); - - setupBindingAndAssert("enable-observation.1", binder); - } - - @SuppressWarnings("rawtypes") - @Test - void testObservationEnabledThroughProducerMessageHandlerCustomizer() throws Exception { - AbstractKafkaTestBinder binder = getBinder(); - KafkaMessageChannelBinder kafkaMessageChannelBinder = binder.getCoreBinder(); - kafkaMessageChannelBinder.setProducerMessageHandlerCustomizer( - (ProducerMessageHandlerCustomizer) (handler, destinationName) -> - handler.getKafkaTemplate().setObservationEnabled(true)); - - setupBindingAndAssert("enable-observation.2", binder); - } - - @Test - void testDynamicPartitionUpdates() throws Exception { - Binder binder = getBinder(); - ExtendedProducerProperties properties = createProducerProperties(); - properties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("headers['partitionKey']")); - properties.setDynamicPartitionUpdatesEnabled(true); - properties.getExtension().getConfiguration().put(ProducerConfig.METADATA_MAX_AGE_CONFIG, "1000"); - - DirectChannel outputChannel = createBindableChannel("output", - createProducerBindingProperties(createProducerProperties())); - - invokeCreateTopic("partitionTopic", 7, 1); - - Binding producerBinding = binder.bindProducer("partitionTopic", - outputChannel, properties); - - KafkaMessageChannelBinder.ProducerConfigurationMessageHandler kafkaProducerMessageHandler = - (KafkaMessageChannelBinder.ProducerConfigurationMessageHandler) TestUtils.getPropertyValue( - producerBinding, "lifecycle", KafkaProducerMessageHandler.class); - - Field kafkaPartitionHandlerField = ReflectionUtils.findField( - KafkaMessageChannelBinder.ProducerConfigurationMessageHandler.class, "kafkaPartitionHandler"); - - PartitionHandler partitionHandler = - (PartitionHandler) kafkaPartitionHandlerField.get(kafkaProducerMessageHandler); - - assertThat(partitionHandler).isNotNull(); - PartitionHandler kafkaPartitionHandlerSpy = spy(partitionHandler); - - kafkaPartitionHandlerField.set(kafkaProducerMessageHandler, kafkaPartitionHandlerSpy); - - // send message with initial partition size - Message message = MessageBuilder - .withPayload("partitionTopic").setHeader("partitionKey", "123").build(); - outputChannel.send(message); - - // change partition size - Map counts = new HashMap<>(); - counts.put("partitionTopic", NewPartitions.increaseTo(11)); - adminClient.createPartitions(counts); - - // wait until metadata is processed in the background - Thread.sleep(2000); - - // send message again with new partition size - Message message2 = MessageBuilder - .withPayload("partitionTopic").setHeader("partitionKey", "456").build(); - outputChannel.send(message2); - - verify(kafkaPartitionHandlerSpy).setPartitionCount(7); - verify(kafkaPartitionHandlerSpy).setPartitionCount(11); - verify(kafkaPartitionHandlerSpy, times(2)).determinePartition(ArgumentMatchers.any()); - } - - private void setupBindingAndAssert(String bindingName, AbstractKafkaTestBinder binder) throws Exception { - ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) binder.getApplicationContext(); - TestObservationRegistry observationRegistry = TestObservationRegistry.create(); - - applicationContext.getBeanFactory().registerSingleton("test-registry", observationRegistry); - - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = new ExtendedProducerProperties<>( - new KafkaProducerProperties()); - Binding producerBinding = binder.bindProducer(bindingName, - moduleOutputChannel, producerProps); - - assertionsOnKafkaTemplate(observationRegistry, producerBinding); - } - - @SuppressWarnings("rawtypes") - private static void assertionsOnKafkaTemplate(TestObservationRegistry observationRegistry, Binding producerBinding) { - KafkaProducerMessageHandler endpoint = TestUtils.getPropertyValue(producerBinding, - "lifecycle", KafkaProducerMessageHandler.class); - - final KafkaTemplate kafkaTemplate = (KafkaTemplate) new DirectFieldAccessor(endpoint).getPropertyValue("kafkaTemplate"); - assertThat(kafkaTemplate).isNotNull(); - Boolean observationEnabled = (Boolean) new DirectFieldAccessor(kafkaTemplate).getPropertyValue("observationEnabled"); - assertThat(observationEnabled).isTrue(); - - ObservationRegistry observationRegistry1 = (ObservationRegistry) new DirectFieldAccessor(kafkaTemplate).getPropertyValue("observationRegistry"); - assertThat(observationRegistry).isSameAs(observationRegistry1); - - producerBinding.unbind(); - } - - private final class FailingInvocationCountingMessageHandler - implements MessageHandler { - - private volatile int invocationCount; - - private final LinkedHashMap> receivedMessages = new LinkedHashMap<>(); - - private final CountDownLatch latch; - - private FailingInvocationCountingMessageHandler(int latchSize) { - latch = new CountDownLatch(latchSize); - } - - private FailingInvocationCountingMessageHandler() { - this(1); - } - - @Override - public void handleMessage(Message message) throws MessagingException { - invocationCount++; - Long offset = message.getHeaders() - .get(KafkaBinderTests.this.getKafkaOffsetHeaderKey(), Long.class); - // using the offset as key allows to ensure that we don't store duplicate - // messages on retry - if (!receivedMessages.containsKey(offset)) { - receivedMessages.put(offset, message); - latch.countDown(); - } - throw new RuntimeException("fail"); - } - - public LinkedHashMap> getReceivedMessages() { - return receivedMessages; - } - - public int getInvocationCount() { - return invocationCount; - } - - public CountDownLatch getLatch() { - return latch; - } - - } - - public static class Pojo { - - private String field; - - public Pojo() { - super(); - } - - public Pojo(String field) { - this.field = field; - } - - public String getField() { - return this.field; - } - - public void setField(String field) { - this.field = field; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderUnitTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderUnitTests.java deleted file mode 100644 index 6c9e0a4c9..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderUnitTests.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka; - -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.serialization.Deserializer; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.messaging.MessageChannel; - -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.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 - * @since 1.2.2 - * - */ -public class KafkaBinderUnitTests { - - @Test - void testPropertyOverrides() throws Exception { - KafkaProperties kafkaProperties = new TestKafkaProperties(); - KafkaBinderConfigurationProperties binderConfigurationProperties = new KafkaBinderConfigurationProperties( - kafkaProperties); - KafkaTopicProvisioner provisioningProvider = new KafkaTopicProvisioner( - binderConfigurationProperties, kafkaProperties, prop -> { - }); - KafkaMessageChannelBinder binder = new KafkaMessageChannelBinder( - binderConfigurationProperties, provisioningProvider); - KafkaConsumerProperties consumerProps = new KafkaConsumerProperties(); - ExtendedConsumerProperties ecp = new ExtendedConsumerProperties( - consumerProps); - Method method = KafkaMessageChannelBinder.class.getDeclaredMethod( - "createKafkaConsumerFactory", boolean.class, String.class, - ExtendedConsumerProperties.class, String.class, String.class); - method.setAccessible(true); - - // test default for anon - Object factory = method.invoke(binder, true, "foo-1", ecp, "foo.consumer", "foo"); - Map configs = TestUtils.getPropertyValue(factory, "configs", Map.class); - assertThat(configs.get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)) - .isEqualTo("latest"); - - // test default for named - factory = method.invoke(binder, false, "foo-2", ecp, "foo.consumer", "foo"); - configs = TestUtils.getPropertyValue(factory, "configs", Map.class); - assertThat(configs.get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)) - .isEqualTo("earliest"); - - // binder level setting - binderConfigurationProperties.setConfiguration(Collections - .singletonMap(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest")); - factory = method.invoke(binder, false, "foo-3", ecp, "foo.consumer", "foo"); - configs = TestUtils.getPropertyValue(factory, "configs", Map.class); - assertThat(configs.get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)) - .isEqualTo("latest"); - - // consumer level setting - consumerProps.setConfiguration(Collections - .singletonMap(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")); - factory = method.invoke(binder, false, "foo-4", ecp, "foo.consumer", "foo"); - configs = TestUtils.getPropertyValue(factory, "configs", Map.class); - assertThat(configs.get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)) - .isEqualTo("earliest"); - } - - @Test - void testMergedConsumerProperties() { - KafkaProperties bootProps = new TestKafkaProperties(); - bootProps.getConsumer().getProperties() - .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "bar"); - KafkaBinderConfigurationProperties props = new KafkaBinderConfigurationProperties( - bootProps); - assertThat(props.mergedConsumerConfiguration() - .get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)).isEqualTo("bar"); - props.getConfiguration().put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "baz"); - assertThat(props.mergedConsumerConfiguration() - .get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)).isEqualTo("baz"); - props.getConsumerProperties().put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "qux"); - assertThat(props.mergedConsumerConfiguration() - .get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)).isEqualTo("qux"); - } - - @Test - void testMergedProducerProperties() { - KafkaProperties bootProps = new TestKafkaProperties(); - bootProps.getProducer().getProperties().put(ProducerConfig.RETRIES_CONFIG, "bar"); - KafkaBinderConfigurationProperties props = new KafkaBinderConfigurationProperties( - bootProps); - assertThat(props.mergedProducerConfiguration().get(ProducerConfig.RETRIES_CONFIG)) - .isEqualTo("bar"); - props.getConfiguration().put(ProducerConfig.RETRIES_CONFIG, "baz"); - assertThat(props.mergedProducerConfiguration().get(ProducerConfig.RETRIES_CONFIG)) - .isEqualTo("baz"); - props.getProducerProperties().put(ProducerConfig.RETRIES_CONFIG, "qux"); - assertThat(props.mergedProducerConfiguration().get(ProducerConfig.RETRIES_CONFIG)) - .isEqualTo("qux"); - } - - @Test - void testOffsetResetWithGroupManagementEarliest() throws Exception { - testOffsetResetWithGroupManagement(true, true, "foo-100", - "testOffsetResetWithGroupManagementEarliest"); - } - - @Test - void testOffsetResetWithGroupManagementLatest() throws Throwable { - testOffsetResetWithGroupManagement(false, true, "foo-101", - "testOffsetResetWithGroupManagementLatest"); - } - - @Test - void testOffsetResetWithManualAssignmentEarliest() throws Exception { - testOffsetResetWithGroupManagement(true, false, "foo-102", - "testOffsetResetWithManualAssignmentEarliest"); - } - - @Test - void testOffsetResetWithGroupManualAssignmentLatest() throws Throwable { - testOffsetResetWithGroupManagement(false, false, "foo-103", - "testOffsetResetWithGroupManualAssignmentLatest"); - } - - void testOffsetResetWithGroupManagement(final boolean earliest, - boolean groupManage, String topic, String group) throws Exception { - - final List partitions = new ArrayList<>(); - partitions.add(new TopicPartition(topic, 0)); - partitions.add(new TopicPartition(topic, 1)); - KafkaBinderConfigurationProperties configurationProperties = new KafkaBinderConfigurationProperties( - new TestKafkaProperties()); - KafkaTopicProvisioner provisioningProvider = mock(KafkaTopicProvisioner.class); - ConsumerDestination dest = mock(ConsumerDestination.class); - given(dest.getName()).willReturn(topic); - given(provisioningProvider.provisionConsumerDestination(anyString(), anyString(), - any())).willReturn(dest); - final AtomicInteger part = new AtomicInteger(); - willAnswer(i -> { - return partitions.stream().map(p -> new PartitionInfo(topic, - part.getAndIncrement(), null, null, null)) - .collect(Collectors.toList()); - }).given(provisioningProvider).getPartitionsForTopic(anyInt(), anyBoolean(), - any(), any()); - @SuppressWarnings("unchecked") - final Consumer consumer = mock(Consumer.class); - final CountDownLatch latch = new CountDownLatch(1); - willAnswer(i -> { - try { - Thread.sleep(100); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - return new ConsumerRecords<>(Collections.emptyMap()); - }).given(consumer).poll(any(Duration.class)); - willAnswer(i -> { - ((org.apache.kafka.clients.consumer.ConsumerRebalanceListener) i - .getArgument(1)).onPartitionsAssigned(partitions); - latch.countDown(); - return null; - }).given(consumer).subscribe(eq(Collections.singletonList(topic)), any()); - willAnswer(i -> { - latch.countDown(); - return null; - }).given(consumer).seekToBeginning(any()); - willAnswer(i -> { - latch.countDown(); - return null; - }).given(consumer).seekToEnd(any()); - class Customizer implements ListenerContainerCustomizer> { - - @Override - public void configure(AbstractMessageListenerContainer container, String destinationName, - String group) { - - container.getContainerProperties().setMissingTopicsFatal(false); - } - - } - KafkaMessageChannelBinder binder = new KafkaMessageChannelBinder( - configurationProperties, provisioningProvider, new Customizer(), null) { - - @Override - protected ConsumerFactory createKafkaConsumerFactory(boolean anonymous, - String consumerGroup, - ExtendedConsumerProperties consumerProperties, String beanName, String destination) { - - return new ConsumerFactory() { - - @Override - public Consumer createConsumer() { - return consumer; - } - - @Override - public Consumer createConsumer(String arg0) { - return consumer; - } - - @Override - public Consumer createConsumer(String arg0, - String arg1) { - return consumer; - } - - @Override - public Consumer createConsumer(String groupId, - String clientIdPrefix, String clientIdSuffix) { - return consumer; - } - - @Override - public boolean isAutoCommit() { - return false; - } - - @Override - public Map getConfigurationProperties() { - Map props = new HashMap<>(); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, - earliest ? "earliest" : "latest"); - props.put(ConsumerConfig.GROUP_ID_CONFIG, group); - return props; - } - - @Override - public Deserializer getKeyDeserializer() { - return null; - } - - @Override - public Deserializer getValueDeserializer() { - return null; - } - - }; - } - - }; - GenericApplicationContext context = new GenericApplicationContext(); - context.refresh(); - binder.setApplicationContext(context); - MessageChannel channel = new DirectChannel(); - KafkaConsumerProperties extension = new KafkaConsumerProperties(); - extension.setResetOffsets(true); - extension.setAutoRebalanceEnabled(groupManage); - ExtendedConsumerProperties consumerProperties = new ExtendedConsumerProperties<>( - extension); - consumerProperties.setInstanceCount(1); - Binding messageChannelBinding = binder.bindConsumer(topic, group, - channel, consumerProperties); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - - if (!groupManage) { - @SuppressWarnings("unchecked") - ArgumentCaptor> captor = ArgumentCaptor.forClass(Set.class); - if (earliest) { - verify(consumer).seekToBeginning(captor.capture()); - } - else { - verify(consumer).seekToEnd(captor.capture()); - } - assertThat(captor.getValue()).containsExactlyInAnyOrderElementsOf(partitions); - } - else { - @SuppressWarnings("unchecked") - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); - if (earliest) { - verify(consumer).seekToBeginning(captor.capture()); - } - else { - verify(consumer).seekToEnd(captor.capture()); - } - assertThat(captor.getValue()).containsExactlyInAnyOrderElementsOf(partitions); - } - messageChannelBinding.unbind(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaTestBinder.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaTestBinder.java deleted file mode 100644 index c09249698..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaTestBinder.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.cloud.stream.binder.kafka.utils.DlqDestinationResolver; -import org.springframework.cloud.stream.binder.kafka.utils.DlqPartitionFunction; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.integration.config.EnableIntegration; -import org.springframework.kafka.support.LoggingProducerListener; -import org.springframework.kafka.support.ProducerListener; - -/** - * Test support class for {@link KafkaMessageChannelBinder}. - * - * @author Eric Bottard - * @author Marius Bogoevici - * @author David Turanski - * @author Gary Russell - * @author Soby Chacko - */ -public class KafkaTestBinder extends AbstractKafkaTestBinder { - - KafkaTestBinder(KafkaBinderConfigurationProperties binderConfiguration, - KafkaTopicProvisioner kafkaTopicProvisioner) { - - this(binderConfiguration, kafkaTopicProvisioner, null, null); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - KafkaTestBinder(KafkaBinderConfigurationProperties binderConfiguration, - KafkaTopicProvisioner kafkaTopicProvisioner, DlqPartitionFunction dlqPartitionFunction, - DlqDestinationResolver dlqDestinationResolver) { - - try { - KafkaMessageChannelBinder binder = new KafkaMessageChannelBinder( - binderConfiguration, kafkaTopicProvisioner, null, null, null, dlqPartitionFunction, dlqDestinationResolver) { - - /* - * Some tests use multiple instance indexes for the same topic; we need to - * make the error infrastructure beans unique. - */ - @Override - protected String errorsBaseName(ConsumerDestination destination, - String group, - ExtendedConsumerProperties consumerProperties) { - return super.errorsBaseName(destination, group, consumerProperties) - + "-" + consumerProperties.getInstanceIndex(); - } - - }; - - ProducerListener producerListener = new LoggingProducerListener(); - binder.setProducerListener(producerListener); - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - Config.class); - setApplicationContext(context); - binder.setApplicationContext(context); - binder.afterPropertiesSet(); - this.setPollableConsumerBinder(binder); - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Configuration - @EnableIntegration - static class Config { - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaTransactionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaTransactionTests.java deleted file mode 100644 index c3bbd27c4..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaTransactionTests.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka; - -import java.util.Collections; - -import org.apache.kafka.clients.producer.Callback; -import org.apache.kafka.clients.producer.Producer; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.TopicPartition; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.cloud.stream.binder.kafka.provisioning.KafkaTopicProvisioner; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.concurrent.SettableListenableFuture; - -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.willReturn; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - -/** - * @author Gary Russell - * @since 2.0 - * - */ -@EmbeddedKafka(count = 1, controlledShutdown = true, brokerProperties = {"transaction.state.log.replication.factor=1", - "transaction.state.log.min.isr=1"}) -public class KafkaTransactionTests { - - private static EmbeddedKafkaBroker embeddedKafka; - - @BeforeAll - public static void setup() { - embeddedKafka = EmbeddedKafkaCondition.getBroker(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testProducerRunsInTx() { - KafkaProperties kafkaProperties = new TestKafkaProperties(); - kafkaProperties.setBootstrapServers(Collections - .singletonList(embeddedKafka.getBrokersAsString())); - KafkaBinderConfigurationProperties configurationProperties = new KafkaBinderConfigurationProperties( - kafkaProperties); - configurationProperties.getTransaction().setTransactionIdPrefix("foo-"); - configurationProperties.getTransaction().getProducer().setUseNativeEncoding(true); - KafkaTopicProvisioner provisioningProvider = new KafkaTopicProvisioner( - configurationProperties, kafkaProperties, prop -> { - }); - provisioningProvider.setMetadataRetryOperations(new RetryTemplate()); - final Producer mockProducer = mock(Producer.class); - given(mockProducer.send(any(), any())).willReturn(new SettableListenableFuture<>()); - - KafkaProducerProperties extension1 = configurationProperties - .getTransaction().getProducer().getExtension(); - extension1.getConfiguration().put(ProducerConfig.RETRIES_CONFIG, "1"); - extension1.getConfiguration().put(ProducerConfig.ACKS_CONFIG, "all"); - - willReturn(Collections.singletonList(new TopicPartition("foo", 0))) - .given(mockProducer).partitionsFor(anyString()); - KafkaMessageChannelBinder binder = new KafkaMessageChannelBinder( - configurationProperties, provisioningProvider) { - - @Override - protected DefaultKafkaProducerFactory getProducerFactory( - String transactionIdPrefix, - ExtendedProducerProperties producerProperties, String beanName, String destination) { - DefaultKafkaProducerFactory producerFactory = spy( - super.getProducerFactory(transactionIdPrefix, - producerProperties, beanName, destination)); - willReturn(mockProducer).given(producerFactory).createProducer("foo-"); - return producerFactory; - } - - }; - GenericApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.refresh(); - binder.setApplicationContext(applicationContext); - DirectChannel channel = new DirectChannel(); - KafkaProducerProperties extension = new KafkaProducerProperties(); - ExtendedProducerProperties properties = new ExtendedProducerProperties<>( - extension); - binder.bindProducer("foo", channel, properties); - channel.send(new GenericMessage<>("foo".getBytes())); - InOrder inOrder = inOrder(mockProducer); - inOrder.verify(mockProducer).beginTransaction(); - inOrder.verify(mockProducer).send(any(ProducerRecord.class), any(Callback.class)); - inOrder.verify(mockProducer).commitTransaction(); - inOrder.verify(mockProducer).close(any()); - inOrder.verifyNoMoreInteractions(); - assertThat(TestUtils.getPropertyValue(channel, - "dispatcher.theOneHandler.useNativeEncoding", Boolean.class)).isTrue(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/MultipleOutputBindingsPartitionsTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/MultipleOutputBindingsPartitionsTests.java deleted file mode 100644 index 982ce0536..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/MultipleOutputBindingsPartitionsTests.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka; - -import java.util.Map; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import reactor.core.publisher.Flux; -import reactor.core.publisher.UnicastProcessor; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.PartitionSelectorStrategy; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/*** - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -@EmbeddedKafka(topics = { "odd-topic", "even-topic" }) -class MultipleOutputBindingsPartitionsTests { - - @Autowired - private EmbeddedKafkaBroker embeddedKafka; - - @Test - void singleInputTupleOutputWithDifferentPartitions() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(MultiOutputApplication.class) - .web(WebApplicationType.NONE).run( - "--server.port=0", - "--spring.jmx.enabled=false", - "--spring.kafka.consumer.metadata.max.age.ms=1000", - "--spring.cloud.function.definition=singleInputMultipleOutputs", - "--spring.cloud.stream.function.reactive.singleInputMultipleOutputs=true", - "--spring.cloud.stream.kafka.binder.autoAddPartitions=true", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-in-0.group=grp5", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-in-0.destination=multi-input", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-0.destination=odd-topic", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-1.destination=even-topic", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-0.producer.partitionKeyExpression=payload", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-0.producer.partitionCount=10", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-0.producer.partitionSelectorName=mySelectorStrategy", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-1.producer.partitionKeyExpression=payload", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-1.producer.partitionCount=5", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-1.producer.partitionSelectorName=mySelectorStrategy", - "--spring.cloud.stream.kafka.binder.brokers=" + embeddedKafka.getBrokersAsString())) { - - StreamBridge streamBridge = context.getBean(StreamBridge.class); - streamBridge.send("multi-input", MessageBuilder.withPayload(101) - .build()); - streamBridge.send("multi-input", MessageBuilder.withPayload(102) - .build()); - - Map consumerProps = KafkaTestUtils.consumerProps("group3", "false", embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - Consumer consumer1 = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer1, "odd-topic"); - Consumer consumer2 = cf.createConsumer("group4", null); - embeddedKafka.consumeFromEmbeddedTopics(consumer2, "even-topic"); - - ConsumerRecord record1 = KafkaTestUtils.getSingleRecord(consumer1, "odd-topic"); - assertThat(record1) - .isNotNull() - .extracting(ConsumerRecord::value) - .isEqualTo("ODD: 101"); - assertThat(record1) - .extracting(ConsumerRecord::partition) - .isEqualTo(9); - - ConsumerRecord record2 = KafkaTestUtils.getSingleRecord(consumer2, "even-topic"); - assertThat(record2) - .isNotNull() - .extracting(ConsumerRecord::value) - .isEqualTo("EVEN: 102"); - assertThat(record2) - .extracting(ConsumerRecord::partition) - .isEqualTo(4); - } - } - - - @EnableAutoConfiguration - public static class MultiOutputApplication { - - @Bean - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static Function, Tuple2, Flux>> singleInputMultipleOutputs() { - return flux -> { - Flux connectedFlux = flux.publish().autoConnect(2); - UnicastProcessor odd = UnicastProcessor.create(); - UnicastProcessor even = UnicastProcessor.create(); - Flux oddFlux = connectedFlux.filter(number -> number % 2 != 0).doOnNext(number -> odd.onNext("ODD: " + number)); - Flux evenFlux = connectedFlux.filter(number -> number % 2 == 0).doOnNext(number -> even.onNext("EVEN: " + number)); - return Tuples.of(Flux.from(odd).doOnSubscribe(x -> oddFlux.subscribe()), Flux.from(even).doOnSubscribe(x -> evenFlux.subscribe())); - }; - } - - @Bean - public PartitionSelectorStrategy mySelectorStrategy() { - return new MyPartitionSelector(); - } - } - - static class MyPartitionSelector implements PartitionSelectorStrategy { - - @Override - public int selectPartition(Object key, int partitionCount) { - // selecting the last partition for easy test verification. - return partitionCount - 1; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawKafkaPartitionTestSupport.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawKafkaPartitionTestSupport.java deleted file mode 100644 index 4d98f495c..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawKafkaPartitionTestSupport.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014-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.cloud.stream.binder.kafka; - -import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy; -import org.springframework.cloud.stream.binder.PartitionSelectorStrategy; -import org.springframework.messaging.Message; - -/** - * @author Marius Bogoevici - */ -public class RawKafkaPartitionTestSupport - implements PartitionKeyExtractorStrategy, PartitionSelectorStrategy { - - @Override - public int selectPartition(Object key, int divisor) { - return ((byte[]) key)[0] % divisor; - } - - @Override - public Object extractKey(Message message) { - return message.getPayload(); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/TestKafkaProperties.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/TestKafkaProperties.java deleted file mode 100644 index 0c64b8bc2..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/TestKafkaProperties.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka; - -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.apache.kafka.common.serialization.ByteArraySerializer; - -import org.springframework.boot.autoconfigure.kafka.KafkaProperties; - -/** - * Test {@link KafkaProperties} initialized in the same way as the - * {@code KafkaBinderEnvironmentPostProcessor} initializes the properties. - * - * @author Gary Russell - * @since 2.1 - * - */ -public class TestKafkaProperties extends KafkaProperties { - - public TestKafkaProperties() { - getConsumer().setKeyDeserializer(ByteArrayDeserializer.class); - getConsumer().setValueDeserializer(ByteArrayDeserializer.class); - getProducer().setKeySerializer(ByteArraySerializer.class); - getProducer().setValueSerializer(ByteArraySerializer.class); - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderBootstrapTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderBootstrapTest.java deleted file mode 100644 index 7433f4b95..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderBootstrapTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.kafka.bootstrap; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -/** - * Integration tests to verify the bootstrap of a SpringBoot application using the Kafka binder. - * - * @author Marius Bogoevici - * @author Chris Bono - */ -@EmbeddedKafka(controlledShutdown = true) -class KafkaBinderBootstrapTest { - - private static final EmbeddedKafkaBroker embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void kafkaBinderWithStandardConfigCanStart(boolean excludeKafkaAutoConfig) { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleApplication.class) - .web(WebApplicationType.NONE).run( - "--spring.cloud.stream.kafka.binder.brokers=" + embeddedKafka.getBrokersAsString(), - excludeKafkaAutoConfigParam(excludeKafkaAutoConfig))) { // @checkstyle:off - } // @checkstyle:on - - } - - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void kafkaBinderWithCustomConfigCanStart(boolean excludeKafkaAutoConfig) { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleApplication.class) - .web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.uppercase-in-0.destination=inputTopic", - "--spring.cloud.stream.bindings.uppercase-in-0.group=inputGroup", - "--spring.cloud.stream.bindings.uppercase-in-0.binder=kafka1", - "--spring.cloud.stream.bindings.uppercase-out-0.destination=outputTopic", - "--spring.cloud.stream.bindings.uppercase-out-0.binder=kafka2", - "--spring.cloud.stream.binders.kafka1.type=kafka", - "--spring.cloud.stream.binders.kafka2.type=kafka", - "--spring.cloud.stream.binders.kafka1.environment" - + ".spring.cloud.stream.kafka.binder.brokers=" + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.binders.kafka2.environment" - + ".spring.cloud.stream.kafka.binder.brokers=" + embeddedKafka.getBrokersAsString(), - excludeKafkaAutoConfigParam(excludeKafkaAutoConfig))) { // @checkstyle:off - } // @checkstyle:on - - } - - private String excludeKafkaAutoConfigParam(boolean excludeKafkaAutoConfig) { - return excludeKafkaAutoConfig ? - "--spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration" : "a=a"; - } - - @SpringBootApplication - static class SimpleApplication { - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderCustomHealthCheckTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderCustomHealthCheckTests.java deleted file mode 100644 index f4dadab51..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderCustomHealthCheckTests.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.kafka.bootstrap; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.kafka.KafkaBinderHealth; -import org.springframework.cloud.stream.binder.kafka.KafkaBinderHealthIndicator; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(count = 1, controlledShutdown = true) -public class KafkaBinderCustomHealthCheckTests { - - private static EmbeddedKafkaBroker embeddedKafka; - - @BeforeAll - public static void setup() { - embeddedKafka = EmbeddedKafkaCondition.getBroker(); - } - - @Test - void testCustomHealthIndicatorIsActivated() { - ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder( - CustomHealthCheckApplication.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.kafka.binder.brokers=" - + embeddedKafka.getBrokersAsString()); - final KafkaBinderHealth kafkaBinderHealth = applicationContext.getBean(KafkaBinderHealth.class); - assertThat(kafkaBinderHealth).isInstanceOf(CustomHealthIndicator.class); - assertThatThrownBy(() -> applicationContext.getBean(KafkaBinderHealthIndicator.class)).isInstanceOf(NoSuchBeanDefinitionException.class); - applicationContext.close(); - } - - @SpringBootApplication - static class CustomHealthCheckApplication { - - @Bean - public CustomHealthIndicator kafkaBinderHealthIndicator() { - return new CustomHealthIndicator(); - } - } - - static class CustomHealthIndicator implements KafkaBinderHealth { - - @Override - public Health health() { - return null; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderMeterRegistryTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderMeterRegistryTest.java deleted file mode 100644 index 88cf55bb6..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/bootstrap/KafkaBinderMeterRegistryTest.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.bootstrap; - -import java.util.Map; -import java.util.function.Function; - -import io.micrometer.core.instrument.MeterRegistry; -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.condition.EmbeddedKafkaCondition; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -/** - * @author Soby Chacko - */ -@EmbeddedKafka(count = 1, controlledShutdown = true, partitions = 10, topics = "outputTopic") -public class KafkaBinderMeterRegistryTest { - - private static EmbeddedKafkaBroker embeddedKafka; - - private static Consumer consumer; - - @BeforeAll - public static void setup() { - embeddedKafka = EmbeddedKafkaCondition.getBroker(); - - Map consumerProps = KafkaTestUtils.consumerProps("group", "false", - embeddedKafka); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); - DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>(consumerProps); - consumer = cf.createConsumer(); - embeddedKafka.consumeFromEmbeddedTopics(consumer, "outputTopic"); - } - - @AfterAll - public static void tearDown() { - consumer.close(); - } - - @Test - void testMetricsWithSingleBinder() throws Exception { - ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(SimpleApplication.class) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.uppercase-in-0.destination=inputTopic", - "--spring.cloud.stream.bindings.uppercase-in-0.group=inputGroup", - "--spring.cloud.stream.bindings.uppercase-out-0.destination=outputTopic", - "--spring.cloud.stream.kafka.binder.brokers" + "=" - + embeddedKafka.getBrokersAsString()); - - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("inputTopic"); - template.sendDefault("foo"); - - // Forcing the retrieval of the data on the outbound so that the producer factory has - // a chance to add the micrometer listener properly. Only on the first send, binder's - // internal KafkaTemplate adds the Micrometer listener (using the producer factory). - KafkaTestUtils.getSingleRecord(consumer, "outputTopic"); - - final MeterRegistry meterRegistry = applicationContext.getBean(MeterRegistry.class); - assertMeterRegistry(meterRegistry); - applicationContext.close(); - } - - @Test - void testMetricsWithMultiBinders() { - ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(SimpleApplication.class) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.uppercase-in-0.destination=inputTopic", - "--spring.cloud.stream.bindings.uppercase-in-0.group=inputGroup", - "--spring.cloud.stream.bindings.uppercase-in-0.binder=kafka1", - "--spring.cloud.stream.bindings.uppercase-out-0.destination=outputTopic", - "--spring.cloud.stream.bindings.uppercase-out-0.binder=kafka2", - "--spring.cloud.stream.binders.kafka1.type=kafka", - "--spring.cloud.stream.binders.kafka2.type=kafka", - "--spring.cloud.stream.binders.kafka1.environment" - + ".spring.cloud.stream.kafka.binder.brokers" + "=" - + embeddedKafka.getBrokersAsString(), - "--spring.cloud.stream.binders.kafka2.environment" - + ".spring.cloud.stream.kafka.binder.brokers" + "=" - + embeddedKafka.getBrokersAsString()); - - Map senderProps = KafkaTestUtils.producerProps(embeddedKafka); - DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>(senderProps); - - KafkaTemplate template = new KafkaTemplate<>(pf, true); - template.setDefaultTopic("inputTopic"); - template.sendDefault("foo"); - - // Forcing the retrieval of the data on the outbound so that the producer factory has - // a chance to add the micrometer listener properly. Only on the first send, binder's - // internal KafkaTemplate adds the Micrometer listener (using the producer factory). - KafkaTestUtils.getSingleRecord(consumer, "outputTopic"); - - final MeterRegistry meterRegistry = applicationContext.getBean(MeterRegistry.class); - assertMeterRegistry(meterRegistry); - applicationContext.close(); - } - - private void assertMeterRegistry(MeterRegistry meterRegistry) { - assertThat(meterRegistry).isNotNull(); - - // assert kafka binder metrics - assertThat(meterRegistry.get("spring.cloud.stream.binder.kafka.offset") - .tag("group", "inputGroup") - .tag("topic", "inputTopic").gauge().value()).isNotNull(); - - // assert consumer metrics - assertThatCode(() -> meterRegistry.get("kafka.consumer.fetch.manager.fetch.total").meter()).doesNotThrowAnyException(); - - // assert producer metrics - assertThatCode(() -> meterRegistry.get("kafka.producer.io.ratio").meter()).doesNotThrowAnyException(); - } - - @SpringBootApplication - static class SimpleApplication { - - @Bean - public Function uppercase() { - return String::toUpperCase; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaBinderActuatorTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaBinderActuatorTests.java deleted file mode 100644 index c16415132..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaBinderActuatorTests.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.integration; - -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.MeterBinder; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter; -import org.springframework.integration.kafka.inbound.KafkaMessageSource; -import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.messaging.MessageChannel; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Artem Bilan - * @author Oleg Zhurakousky - * @author Jon Schneider - * @author Gary Russell - * @author Soby Chacko - * - * @since 2.0 - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, - properties = { - "spring.cloud.stream.bindings.input.group=" + KafkaBinderActuatorTests.TEST_CONSUMER_GROUP, - "spring.cloud.stream.function.bindings.process-in-0=input", - "spring.cloud.stream.pollable-source=input"} -) -@DirtiesContext -@EmbeddedKafka(bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public class KafkaBinderActuatorTests { - - static final String TEST_CONSUMER_GROUP = "testGroup-actuatorTests"; - - @Autowired - private MeterRegistry meterRegistry; - - @Autowired - private KafkaTemplate kafkaTemplate; - - @Test - void testKafkaBinderMetricsExposed() { - this.kafkaTemplate.send("input", null, "foo".getBytes()); - this.kafkaTemplate.flush(); - - assertThat(this.meterRegistry.get("spring.cloud.stream.binder.kafka.offset") - .tag("group", TEST_CONSUMER_GROUP).tag("topic", "input").gauge() - .value()).isGreaterThan(0); - } - - @Test - @Disabled - void testKafkaBinderMetricsWhenNoMicrometer() { - new ApplicationContextRunner().withUserConfiguration(KafkaMetricsTestConfig.class) - .withPropertyValues( - "spring.cloud.stream.bindings.input.group", KafkaBinderActuatorTests.TEST_CONSUMER_GROUP, - "spring.cloud.stream.function.bindings.process-in-0", "input", - "spring.cloud.stream.pollable-source", "input") - .withClassLoader(new FilteredClassLoader("io.micrometer.core")) - .run(context -> { - assertThat(context.getBeanNamesForType(MeterRegistry.class)) - .isEmpty(); - assertThat(context.getBeanNamesForType(MeterBinder.class)).isEmpty(); - - DirectFieldAccessor channelBindingServiceAccessor = new DirectFieldAccessor( - context.getBean(BindingService.class)); - @SuppressWarnings("unchecked") - Map>> consumerBindings = - (Map>>) channelBindingServiceAccessor - .getPropertyValue("consumerBindings"); - assertThat(new DirectFieldAccessor( - consumerBindings.get("input").get(0)).getPropertyValue( - "lifecycle.messageListenerContainer.beanName")) - .isEqualTo("setByCustomizer:input"); - assertThat(new DirectFieldAccessor( - consumerBindings.get("input").get(0)).getPropertyValue( - "lifecycle.beanName")) - .isEqualTo("setByCustomizer:input"); - assertThat(new DirectFieldAccessor( - consumerBindings.get("source").get(0)).getPropertyValue( - "lifecycle.beanName")) - .isEqualTo("setByCustomizer:source"); - - @SuppressWarnings("unchecked") - Map> producerBindings = - (Map>) channelBindingServiceAccessor - .getPropertyValue("producerBindings"); - - assertThat(new DirectFieldAccessor( - producerBindings.get("output")).getPropertyValue( - "lifecycle.beanName")) - .isEqualTo("setByCustomizer:output"); - }); - } - - @EnableAutoConfiguration - @Configuration - public static class KafkaMetricsTestConfig { - - @Bean - public ListenerContainerCustomizer> containerCustomizer() { - return (c, q, g) -> c.setBeanName("setByCustomizer:" + q); - } - - @Bean - public MessageSourceCustomizer> sourceCustomizer() { - return (s, q, g) -> s.setBeanName("setByCustomizer:" + q); - } - - @Bean - public ConsumerEndpointCustomizer> consumerCustomizer() { - return (p, q, g) -> p.setBeanName("setByCustomizer:" + q); - } - - @Bean - public ProducerMessageHandlerCustomizer> handlerCustomizer() { - return (handler, destinationName) -> handler.setBeanName("setByCustomizer:" + destinationName); - } - - @Bean - public Consumer process() { - // Artificial slow listener to emulate consumer lag - return s -> { - try { - Thread.sleep(1000); - } - catch (InterruptedException e) { - //no-op - } - }; - } - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaBinderExtendedPropertiesTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaBinderExtendedPropertiesTest.java deleted file mode 100644 index 6bae85b8f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaBinderExtendedPropertiesTest.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.integration; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.common.TopicPartition; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.kafka.KafkaBindingRebalanceListener; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaConsumerProperties; -import org.springframework.cloud.stream.binder.kafka.properties.KafkaProducerProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.messaging.MessageChannel; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - * @author Gary Russell - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { - "spring.cloud.function.definition=process;processCustom", - "spring.cloud.stream.function.bindings.process-in-0=standard-in", - "spring.cloud.stream.function.bindings.process-out-0=standard-out", - "spring.cloud.stream.function.bindings.processCustom-in-0=custom-in", - "spring.cloud.stream.function.bindings.processCustom-out-0=custom-out", - "spring.cloud.stream.kafka.bindings.standard-out.producer.configuration.key.serializer=FooSerializer.class", - "spring.cloud.stream.kafka.default.producer.configuration.key.serializer=BarSerializer.class", - "spring.cloud.stream.kafka.default.producer.configuration.value.serializer=BarSerializer.class", - "spring.cloud.stream.kafka.bindings.standard-in.consumer.configuration.key.serializer=FooSerializer.class", - "spring.cloud.stream.kafka.default.consumer.configuration.key.serializer=BarSerializer.class", - "spring.cloud.stream.kafka.default.consumer.configuration.value.serializer=BarSerializer.class", - "spring.cloud.stream.kafka.default.producer.configuration.foo=bar", - "spring.cloud.stream.kafka.bindings.standard-out.producer.configuration.foo=" - + "bindingSpecificPropertyShouldWinOverDefault", - "spring.cloud.stream.kafka.default.consumer.ackEachRecord=true", - "spring.cloud.stream.kafka.bindings.custom-in.consumer.ackEachRecord=false" }) -@DirtiesContext -@EmbeddedKafka(bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public class KafkaBinderExtendedPropertiesTest { - - @Autowired - private ConfigurableApplicationContext context; - - @Test - void testKafkaBinderExtendedProperties() throws Exception { - - BinderFactory binderFactory = context.getBeanFactory() - .getBean(BinderFactory.class); - Binder kafkaBinder = binderFactory - .getBinder("kafka", MessageChannel.class); - - KafkaProducerProperties kafkaProducerProperties = (KafkaProducerProperties) ((ExtendedPropertiesBinder) kafkaBinder) - .getExtendedProducerProperties("standard-out"); - - // binding "standard-out" gets FooSerializer defined on the binding itself - // and BarSerializer through default property. - assertThat(kafkaProducerProperties.getConfiguration().get("key.serializer")) - .isEqualTo("FooSerializer.class"); - assertThat(kafkaProducerProperties.getConfiguration().get("value.serializer")) - .isEqualTo("BarSerializer.class"); - - assertThat(kafkaProducerProperties.getConfiguration().get("foo")) - .isEqualTo("bindingSpecificPropertyShouldWinOverDefault"); - - // @checkstyle:off - KafkaConsumerProperties kafkaConsumerProperties = (KafkaConsumerProperties) ((ExtendedPropertiesBinder) kafkaBinder) - .getExtendedConsumerProperties("standard-in"); - // @checkstyle:on - // binding "standard-in" gets FooSerializer defined on the binding itself - // and BarSerializer through default property. - assertThat(kafkaConsumerProperties.getConfiguration().get("key.serializer")) - .isEqualTo("FooSerializer.class"); - assertThat(kafkaConsumerProperties.getConfiguration().get("value.serializer")) - .isEqualTo("BarSerializer.class"); - - // @checkstyle:off - KafkaProducerProperties customKafkaProducerProperties = (KafkaProducerProperties) ((ExtendedPropertiesBinder) kafkaBinder) - .getExtendedProducerProperties("custom-out"); - // @checkstyle:on - - // binding "standard-out" gets BarSerializer and BarSerializer for - // key.serializer/value.serializer through default properties. - assertThat(customKafkaProducerProperties.getConfiguration().get("key.serializer")) - .isEqualTo("BarSerializer.class"); - assertThat( - customKafkaProducerProperties.getConfiguration().get("value.serializer")) - .isEqualTo("BarSerializer.class"); - - // through default properties. - assertThat(customKafkaProducerProperties.getConfiguration().get("foo")) - .isEqualTo("bar"); - - // @checkstyle:off - KafkaConsumerProperties customKafkaConsumerProperties = (KafkaConsumerProperties) ((ExtendedPropertiesBinder) kafkaBinder) - .getExtendedConsumerProperties("custom-in"); - // @checkstyle:on - // binding "standard-in" gets BarSerializer and BarSerializer for - // key.serializer/value.serializer through default properties. - assertThat(customKafkaConsumerProperties.getConfiguration().get("key.serializer")) - .isEqualTo("BarSerializer.class"); - assertThat( - customKafkaConsumerProperties.getConfiguration().get("value.serializer")) - .isEqualTo("BarSerializer.class"); - - assertThat(kafkaConsumerProperties.isAckEachRecord()).isEqualTo(true); - assertThat(customKafkaConsumerProperties.isAckEachRecord()).isEqualTo(false); - - RebalanceListener rebalanceListener = context.getBean(RebalanceListener.class); - assertThat(rebalanceListener.latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(rebalanceListener.bindings.keySet()).contains("standard-in", - "custom-in"); - assertThat(rebalanceListener.bindings.values()).containsExactly(Boolean.TRUE, - Boolean.TRUE); - } - - @EnableAutoConfiguration - @Configuration - public static class KafkaMetricsTestConfig { - - @Bean - public Function process() { - return payload -> payload; - } - - @Bean - public Function processCustom() { - return payload -> payload; - } - @Bean - public RebalanceListener rebalanceListener() { - return new RebalanceListener(); - } - - } - - public static class RebalanceListener implements KafkaBindingRebalanceListener { - - private final Map bindings = new HashMap<>(); - - private final CountDownLatch latch = new CountDownLatch(2); - - @Override - public void onPartitionsRevokedBeforeCommit(String bindingName, - Consumer consumer, Collection partitions) { - } - - @Override - public void onPartitionsRevokedAfterCommit(String bindingName, - Consumer consumer, Collection partitions) { - } - - @Override - public void onPartitionsAssigned(String bindingName, Consumer consumer, - Collection partitions, boolean initial) { - this.bindings.put(bindingName, initial); - this.latch.countDown(); - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaConfigCustomizationTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaConfigCustomizationTests.java deleted file mode 100644 index f82cd8890..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaConfigCustomizationTests.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.kafka.integration; - -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerInterceptor; -import org.apache.kafka.clients.consumer.ConsumerRecords; -import org.apache.kafka.clients.consumer.OffsetAndMetadata; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.clients.producer.ProducerInterceptor; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.clients.producer.RecordMetadata; -import org.apache.kafka.common.TopicPartition; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.kafka.support.ConsumerConfigCustomizer; -import org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - * - * Based on: https://github.com/spring-projects/spring-kafka/issues/897#issuecomment-466060097 - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {"spring.cloud.function.definition=process", - "spring.cloud.stream.bindings.process-in-0.group=KafkaConfigCustomizationTests.group"}) -@DirtiesContext -@EmbeddedKafka(bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public class KafkaConfigCustomizationTests { - - private static final String KAFKA_BROKERS_PROPERTY = "spring.cloud.stream.kafka.binder.brokers"; - - static final CountDownLatch countDownLatch = new CountDownLatch(2); - - @Autowired - EmbeddedKafkaBroker embeddedKafkaBroker; - - @Test - void testBothConsumerAndProducerConfigsCanBeCustomized() throws InterruptedException { - Map producerProps = KafkaTestUtils - .producerProps(embeddedKafkaBroker); - KafkaTemplate template = new KafkaTemplate<>( - new DefaultKafkaProducerFactory<>(producerProps)); - template.send("process-in-0", "test-foo"); - template.flush(); - assertThat(countDownLatch.await(10, TimeUnit.SECONDS)).isTrue(); - } - - @SpringBootApplication - public static class ConfigCustomizerTestConfig { - - @Bean - public Function process() { - return payload -> payload; - } - - @Bean - public ConsumerConfigCustomizer consumerConfigCustomizer() { - return (consumerProperties, binding, destination) -> { - assertThat(binding).isEqualTo("process-in-0"); - assertThat(destination).isEqualTo("process-in-0"); - consumerProperties.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, MyConsumerInterceptor.class.getName()); - consumerProperties.put("foo.bean", foo()); - }; - } - - @Bean - public ProducerConfigCustomizer producerConfigCustomizer() { - return (producerProperties, binding, destination) -> { - assertThat(binding).isEqualTo("process-out-0"); - assertThat(destination).isEqualTo("process-out-0"); - producerProperties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, MyProducerInterceptor.class.getName()); - producerProperties.put("foo.bean", foo()); - }; - } - - @Bean - public Foo foo() { - return new Foo(); - } - } - - public static class Foo { - - public void foo(String what) { - KafkaConfigCustomizationTests.countDownLatch.countDown(); - } - - } - - public static class MyConsumerInterceptor implements ConsumerInterceptor { - - private Foo foo; - - @Override - public void configure(Map configs) { - this.foo = (Foo) configs.get("foo.bean"); - } - - @Override - public ConsumerRecords onConsume(ConsumerRecords records) { - this.foo.foo("consumer interceptor"); - return records; - } - - @Override - public void onCommit(Map offsets) { - - } - - @Override - public void close() { - } - - } - - public static class MyProducerInterceptor implements ProducerInterceptor { - - private Foo foo; - - @Override - public void configure(Map configs) { - this.foo = (Foo) configs.get("foo.bean"); - } - - @Override - public ProducerRecord onSend(ProducerRecord record) { - this.foo.foo("producer interceptor"); - return record; - } - - @Override - public void onAcknowledgement(RecordMetadata metadata, Exception exception) { - } - - @Override - public void close() { - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaNullConverterTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaNullConverterTest.java deleted file mode 100644 index 166bdf45d..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaNullConverterTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.kafka.integration; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.kafka.support.KafkaNull; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Aldo Sinanaj - * @author Gary Russell - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { - "spring.kafka.consumer.auto-offset-reset=earliest", - "spring.cloud.stream.function.bindings.inputListen-in-0=kafkaNullInput"}) -@DirtiesContext -@EmbeddedKafka(bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public class KafkaNullConverterTest { - - @Autowired - private ApplicationContext context; - - @Autowired - private KafkaNullConverterTestConfig config; - - @Test - void testKafkaNullConverterOutput() throws InterruptedException { - final StreamBridge streamBridge = context.getBean(StreamBridge.class); - - streamBridge.send("kafkaNullOutput", new GenericMessage<>(KafkaNull.INSTANCE)); - - assertThat(this.config.countDownLatchOutput.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.outputPayload).isNull(); - } - - @Test - void testKafkaNullConverterInput() throws InterruptedException { - - final MessageChannel kafkaNullInput = context.getBean("kafkaNullInput", MessageChannel.class); - - kafkaNullInput.send(new GenericMessage<>(KafkaNull.INSTANCE)); - - assertThat(this.config.countDownLatchInput.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.inputPayload).isNull(); - } - - @EnableAutoConfiguration - @Configuration - public static class KafkaNullConverterTestConfig { - - final CountDownLatch countDownLatchOutput = new CountDownLatch(1); - - final CountDownLatch countDownLatchInput = new CountDownLatch(1); - - volatile byte[] outputPayload = new byte[0]; - - volatile byte[] inputPayload = new byte[0]; - - @KafkaListener(id = "foo", topics = "kafkaNullOutput") - public void listen(@Payload(required = false) byte[] in) { - this.outputPayload = in; - countDownLatchOutput.countDown(); - } - - @Bean - public Consumer> inputListen() { - return in -> { - Object v = in.getPayload(); - String className = v.getClass().getName(); - if (className.equals("org.springframework.kafka.support.KafkaNull")) { - this.inputPayload = null; - } - countDownLatchInput.countDown(); - }; - } - - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaRetryDlqBinderOrContainerTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaRetryDlqBinderOrContainerTests.java deleted file mode 100644 index 5bd83f38a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/KafkaRetryDlqBinderOrContainerTests.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2021-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.cloud.stream.binder.kafka.integration; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.function.Consumer; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.common.TopicPartition; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.kafka.ListenerContainerWithDlqAndRetryCustomizer; -import org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer; -import org.springframework.cloud.stream.binding.BindingsLifecycleController; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.core.KafkaOperations; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.kafka.listener.CommonErrorHandler; -import org.springframework.kafka.listener.ConsumerRecordRecoverer; -import org.springframework.kafka.listener.DeadLetterPublishingRecoverer; -import org.springframework.kafka.listener.DefaultErrorHandler; -import org.springframework.kafka.support.ExponentialBackOffWithMaxRetries; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.lang.Nullable; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.util.backoff.BackOff; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * @author Gary Russell - * @author Soby Chacko - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { - "spring.cloud.function.definition=retryInBinder;retryInContainer", - "spring.cloud.stream.bindings.retryInBinder-in-0.group=foo", - "spring.cloud.stream.bindings.retryInContainer-in-0.group=bar", - "spring.cloud.stream.kafka.bindings.retryInBinder-in-0.consumer.enable-dlq=true", - "spring.cloud.stream.kafka.bindings.retryInContainer-in-0.consumer.enable-dlq=true"}) -@EmbeddedKafka(bootstrapServersProperty = "spring.kafka.bootstrap-servers") -@DirtiesContext -public class KafkaRetryDlqBinderOrContainerTests { - - private static final CountDownLatch latch = new CountDownLatch(2); - - @Test - void retryAndDlqInRightPlace(@Autowired BindingsLifecycleController controller) throws Exception { - Binding retryInBinder = controller.queryState("retryInBinder-in-0").get(0); - assertThat(KafkaTestUtils.getPropertyValue(retryInBinder, "lifecycle.retryTemplate")).isNotNull(); - assertThat(KafkaTestUtils.getPropertyValue(retryInBinder, - "lifecycle.messageListenerContainer.commonErrorHandler")).isNull(); - Binding retryInContainer = controller.queryState("retryInContainer-in-0").get(0); - assertThat(KafkaTestUtils.getPropertyValue(retryInContainer, "lifecycle.retryTemplate")).isNull(); - assertThat(KafkaTestUtils.getPropertyValue(retryInContainer, - "lifecycle.messageListenerContainer.commonErrorHandler")).isInstanceOf(CommonErrorHandler.class); - assertThat(KafkaTestUtils.getPropertyValue(retryInContainer, - "lifecycle.messageListenerContainer.commonErrorHandler.failureTracker.backOff")) - .isInstanceOf(ExponentialBackOffWithMaxRetries.class); - boolean await = latch.await(5, TimeUnit.SECONDS); - assertThat(await).isTrue(); - - } - - @SpringBootApplication - public static class ConfigCustomizerTestConfig { - - @Bean - public Consumer retryInBinder() { - return str -> { }; - } - - @Bean - public Consumer retryInContainer() { - return str -> { }; - } - - @Bean - ListenerContainerWithDlqAndRetryCustomizer cust() { - return new ListenerContainerWithDlqAndRetryCustomizer() { - - @Override - public void configure(AbstractMessageListenerContainer container, String destinationName, - String group, - BiFunction, Exception, TopicPartition> dlqDestinationResolver, - @Nullable BackOff backOff) { - - if (destinationName.contains("Container")) { - ConsumerRecordRecoverer dlpr = new DeadLetterPublishingRecoverer(mock(KafkaOperations.class), - dlqDestinationResolver); - container.setCommonErrorHandler(new DefaultErrorHandler(dlpr, backOff)); - } - } - - @Override - public boolean retryAndDlqInBinding(String destinationName, String group) { - return !destinationName.contains("Container"); - } - - }; - } - - // Because we have DLQ enabled on the consumer binding, - // this ProducerConfigCustomizer is used by the producer on DLQ. - @Bean - public ProducerConfigCustomizer producerConfigCustomizer() { - return (producerProperties, binding, destination) -> { - if (binding.equals("retryInBinder-in-0")) { - latch.countDown(); - } - else if (binding.equals("retryInContainer-in-0")) { - latch.countDown(); - } - }; - } - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/ProducerOnlyTransactionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/ProducerOnlyTransactionTests.java deleted file mode 100644 index f8f2afdb7..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/ProducerOnlyTransactionTests.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.integration; - -import java.util.Map; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.consumer.KafkaConsumer; -import org.apache.kafka.common.IsolationLevel; -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.kafka.KafkaMessageChannelBinder; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.kafka.transaction.KafkaTransactionManager; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.AbstractPlatformTransactionManager; -import org.springframework.transaction.support.TransactionSynchronizationManager; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Gary Russell - * @author Soby Chacko - * @since 2.1.4 - * - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { - "spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix=tx.", - "spring.cloud.stream.kafka.binder.transaction.producer.configuration.retries=99", - "spring.cloud.stream.kafka.binder.transaction.producer.configuration.acks=all"}) -@DirtiesContext -@EmbeddedKafka(topics = "output", controlledShutdown = true, brokerProperties = {"transaction.state.log.replication.factor=1", - "transaction.state.log.min.isr=1"}, bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public class ProducerOnlyTransactionTests { - - @Autowired - private Sender sender; - - @Autowired - private ApplicationContext context; - - @Autowired - EmbeddedKafkaBroker embeddedKafkaBrokera; - - @Test - void testProducerTx() { - final StreamBridge streamBridge = context.getBean(StreamBridge.class); - this.sender.DoInTransaction(streamBridge); - assertThat(this.sender.isInTx()).isTrue(); - Map props = KafkaTestUtils.consumerProps("consumeTx", "false", - embeddedKafkaBrokera); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, IsolationLevel.READ_COMMITTED.name().toLowerCase()); - Consumer consumer = new KafkaConsumer<>(props); - embeddedKafkaBrokera.consumeFromAllEmbeddedTopics(consumer); - ConsumerRecord record = KafkaTestUtils.getSingleRecord(consumer, "output"); - assertThat(record.value()).isEqualTo("foo".getBytes()); - } - - @EnableAutoConfiguration - @EnableTransactionManagement - @Configuration - public static class Config { - - @Bean - public PlatformTransactionManager transactionManager(BinderFactory binders) { - try { - ProducerFactory pf = ((KafkaMessageChannelBinder) binders.getBinder(null, - MessageChannel.class)).getTransactionalProducerFactory(); - KafkaTransactionManager tm = new KafkaTransactionManager<>(pf); - tm.setTransactionSynchronization(AbstractPlatformTransactionManager.SYNCHRONIZATION_ON_ACTUAL_TRANSACTION); - return tm; - } - catch (BeanCreationException e) { // needed to avoid other tests in this package failing when there is no binder - return null; - } - } - - @Bean - public Sender sender() { - return new Sender(); - } - - } - - public static class Sender { - - private boolean isInTx; - - @Transactional - public void DoInTransaction(StreamBridge streamBridge) { - this.isInTx = TransactionSynchronizationManager.isActualTransactionActive(); - streamBridge.send("output", new GenericMessage<>("foo".getBytes())); - } - - public boolean isInTx() { - return this.isInTx; - } - - } - -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/BaseKafkaBinderTopicPropertiesUpdateTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/BaseKafkaBinderTopicPropertiesUpdateTest.java deleted file mode 100644 index 05801f9a0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/BaseKafkaBinderTopicPropertiesUpdateTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.integration.topic.configs; - -import java.util.function.Function; - -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -import org.springframework.kafka.test.EmbeddedKafkaBroker; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * @author Heiko Does - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest( - classes = BaseKafkaBinderTopicPropertiesUpdateTest.TopicAutoConfigsTestConfig.class, - webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { - "spring.cloud.stream.function.bindings.process-in-0=standard-in", - "spring.cloud.stream.function.bindings.process-out-0=standard-out", - "spring.cloud.stream.kafka.bindings.standard-out.producer.topic.properties.retention.ms=9001", - "spring.cloud.stream.kafka.default.producer.topic.properties.retention.ms=-1", - "spring.cloud.stream.kafka.bindings.standard-in.consumer.topic.properties.retention.ms=9001", - "spring.cloud.stream.kafka.default.consumer.topic.properties.retention.ms=-1" -}) -@DirtiesContext -@EmbeddedKafka(bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public abstract class BaseKafkaBinderTopicPropertiesUpdateTest { - - @Autowired - protected EmbeddedKafkaBroker embeddedKafka; - - @EnableAutoConfiguration - public static class TopicAutoConfigsTestConfig { - - @Bean - public Function process() { - return payload -> payload; - } - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/DisabledKafkaBinderTopicPropertiesUpdateTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/DisabledKafkaBinderTopicPropertiesUpdateTest.java deleted file mode 100644 index a7ca8ec4f..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/DisabledKafkaBinderTopicPropertiesUpdateTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.integration.topic.configs; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.Config; -import org.apache.kafka.clients.admin.DescribeConfigsResult; -import org.apache.kafka.common.KafkaFuture; -import org.apache.kafka.common.config.ConfigResource; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Taras Danylchuk - */ -public class DisabledKafkaBinderTopicPropertiesUpdateTest extends BaseKafkaBinderTopicPropertiesUpdateTest { - - @Test - void testKafkaBinderShouldNotUpdateTopicConfigurationOnDisabledFeature() throws Exception { - Map adminClientConfig = new HashMap<>(); - adminClientConfig.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafka.getBrokersAsString()); - AdminClient adminClient = AdminClient.create(adminClientConfig); - ConfigResource standardInConfigResource = new ConfigResource(ConfigResource.Type.TOPIC, "standard-in"); - ConfigResource standardOutConfigResource = new ConfigResource(ConfigResource.Type.TOPIC, "standard-out"); - DescribeConfigsResult describeConfigsResult = adminClient.describeConfigs(Arrays - .asList(standardInConfigResource, standardOutConfigResource)); - KafkaFuture> kafkaFuture = describeConfigsResult.all(); - Map configResourceConfigMap = kafkaFuture.get(3, TimeUnit.SECONDS); - Config standardInTopicConfig = configResourceConfigMap.get(standardInConfigResource); - assertThat(standardInTopicConfig.get("retention.ms").value()).isEqualTo("9001"); - - Config standardOutTopicConfig = configResourceConfigMap.get(standardOutConfigResource); - assertThat(standardOutTopicConfig.get("retention.ms").value()).isEqualTo("9001"); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/KafkaBinderTopicPropertiesUpdateTest.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/KafkaBinderTopicPropertiesUpdateTest.java deleted file mode 100644 index a1912afd0..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration/topic/configs/KafkaBinderTopicPropertiesUpdateTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder.kafka.integration.topic.configs; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.Config; -import org.apache.kafka.clients.admin.DescribeConfigsResult; -import org.apache.kafka.common.KafkaFuture; -import org.apache.kafka.common.config.ConfigResource; -import org.junit.jupiter.api.Test; - -import org.springframework.test.context.TestPropertySource; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Heiko Does - */ -@TestPropertySource(properties = "spring.cloud.stream.kafka.binder.autoAlterTopics=true") -public class KafkaBinderTopicPropertiesUpdateTest extends BaseKafkaBinderTopicPropertiesUpdateTest { - - @Test - void testKafkaBinderUpdateTopicConfiguration() throws Exception { - Map adminClientConfig = new HashMap<>(); - adminClientConfig.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafka.getBrokersAsString()); - AdminClient adminClient = AdminClient.create(adminClientConfig); - ConfigResource standardInConfigResource = new ConfigResource(ConfigResource.Type.TOPIC, "standard-in"); - ConfigResource standardOutConfigResource = new ConfigResource(ConfigResource.Type.TOPIC, "standard-out"); - DescribeConfigsResult describeConfigsResult = adminClient.describeConfigs(Arrays - .asList(standardInConfigResource, standardOutConfigResource)); - KafkaFuture> kafkaFuture = describeConfigsResult.all(); - Map configResourceConfigMap = kafkaFuture.get(3, TimeUnit.SECONDS); - Config standardInTopicConfig = configResourceConfigMap.get(standardInConfigResource); - assertThat(standardInTopicConfig.get("retention.ms").value()).isEqualTo("9001"); - - Config standardOutTopicConfig = configResourceConfigMap.get(standardOutConfigResource); - assertThat(standardOutTopicConfig.get("retention.ms").value()).isEqualTo("9001"); - } -} diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration2/ConsumerProducerTransactionTests.java b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration2/ConsumerProducerTransactionTests.java deleted file mode 100644 index 15ce8b566..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/integration2/ConsumerProducerTransactionTests.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binder.kafka.integration2; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.listener.AbstractMessageListenerContainer; -import org.springframework.kafka.listener.DefaultAfterRollbackProcessor; -import org.springframework.kafka.test.context.EmbeddedKafka; -import org.springframework.kafka.test.utils.KafkaTestUtils; -import org.springframework.kafka.transaction.KafkaAwareTransactionManager; -import org.springframework.messaging.MessageChannel; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.util.backoff.FixedBackOff; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * @author Gary Russell - * @author Soby Chacko - * @since 3.0 - * - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = { - "spring.kafka.consumer.properties.isolation.level=read_committed", - "spring.kafka.consumer.enable-auto-commit=false", - "spring.kafka.consumer.auto-offset-reset=earliest", - "spring.cloud.function.definition=listenIn;listenIn2", - "spring.cloud.stream.function.bindings.listenIn-in-0=input", - "spring.cloud.stream.function.bindings.listenIn-out-0=output", - "spring.cloud.stream.function.bindings.listenIn2-in-0=input2", - "spring.cloud.stream.function.bindings.listenIn2-out-0=output2", - "spring.cloud.stream.bindings.input.destination=consumer.producer.txIn", - "spring.cloud.stream.bindings.input.group=consumer.producer.tx", - "spring.cloud.stream.bindings.input.consumer.max-attempts=1", - "spring.cloud.stream.kafka.bindings.input2.consumer.transaction-manager=tm", - "spring.cloud.stream.kafka.bindings.output2.producer.transaction-manager=tm", - "spring.cloud.stream.bindings.output.destination=consumer.producer.txOut", - "spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix=tx.", - "spring.cloud.stream.kafka.binder.transaction.producer.configuration.retries=99", - "spring.cloud.stream.kafka.binder.transaction.producer.configuration.acks=all"}) -@DirtiesContext -@EmbeddedKafka(topics = "consumer.producer.txOut", controlledShutdown = true, brokerProperties = {"transaction.state.log.replication.factor=1", - "transaction.state.log.min.isr=1"}, bootstrapServersProperty = "spring.kafka.bootstrap-servers") -public class ConsumerProducerTransactionTests { - - @Autowired - private Config config; - - @Autowired - private ApplicationContext context; - - @Test - public void testProducerRunsInConsumerTransaction() throws InterruptedException { - assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.outs).containsExactlyInAnyOrder("ONE", "THREE"); - } - - @Test - void externalTM() { - assertThat(this.config.input2Container.getContainerProperties().getTransactionManager()) - .isSameAs(this.config.tm); - final MessageChannel output2 = context.getBean("output2", MessageChannel.class); - - Object handler = KafkaTestUtils.getPropertyValue(output2, "dispatcher.handlers", Set.class) - .iterator().next(); - assertThat(KafkaTestUtils.getPropertyValue(handler, "delegate.kafkaTemplate.producerFactory")) - .isSameAs(this.config.pf); - } - - @EnableAutoConfiguration - @Configuration - public static class Config { - - final List outs = new ArrayList<>(); - - final CountDownLatch latch = new CountDownLatch(2); - - AbstractMessageListenerContainer input2Container; - - ProducerFactory pf; - - KafkaAwareTransactionManager tm; - - @KafkaListener(id = "test.cons.prod", topics = "consumer.producer.txOut") - public void listenOut(String in) { - this.outs.add(in); - this.latch.countDown(); - } - - @Bean - public Function listenIn() { - return in -> { - if (in.equals("two")) { - throw new RuntimeException("fail"); - } - return in.toUpperCase(); - }; - } - - @Bean - public Function listenIn2() { - return in -> in; - } - - @Bean - public ApplicationRunner runner(KafkaTemplate template) { - return args -> { - template.send("consumer.producer.txIn", "one".getBytes()); - template.send("consumer.producer.txIn", "two".getBytes()); - template.send("consumer.producer.txIn", "three".getBytes()); - }; - } - - @Bean - public ListenerContainerCustomizer> customizer() { - return (container, dest, group) -> { - container.setAfterRollbackProcessor(new DefaultAfterRollbackProcessor<>(new FixedBackOff(0L, 1L))); - if ("input2".equals(dest)) { - this.input2Container = container; - } - }; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Bean - public KafkaAwareTransactionManager tm(ProducerFactory pf) { - KafkaAwareTransactionManager mock = mock(KafkaAwareTransactionManager.class); - this.pf = pf; - given(mock.getProducerFactory()).willReturn(pf); - this.tm = mock; - return mock; - } - } -} - diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/binder-config-autoconfig.properties b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/binder-config-autoconfig.properties deleted file mode 100644 index 38d0d9f2a..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/binder-config-autoconfig.properties +++ /dev/null @@ -1,10 +0,0 @@ -spring.kafka.producer.keySerializer=org.apache.kafka.common.serialization.LongSerializer -spring.kafka.producer.valueSerializer=org.apache.kafka.common.serialization.LongSerializer -spring.kafka.consumer.keyDeserializer=org.apache.kafka.common.serialization.LongDeserializer -spring.kafka.consumer.valueDeserializer=org.apache.kafka.common.serialization.LongDeserializer -spring.kafka.producer.batchSize=10 -spring.kafka.bootstrapServers=10.98.09.199:9092,10.98.09.196:9092 -spring.kafka.producer.compressionType=snappy -# Test consumer properties -spring.kafka.consumer.auto-offset-reset=earliest -spring.kafka.consumer.group-id=groupIdFromBootConfig diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/binder-config.properties b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/binder-config.properties deleted file mode 100644 index 5a1096324..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/binder-config.properties +++ /dev/null @@ -1 +0,0 @@ -spring.cloud.stream.kafka.binder.brokers=10.98.09.199:9082 diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/jaas-sample-kafka-only.conf b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/jaas-sample-kafka-only.conf deleted file mode 100644 index 9b6891fb8..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/jaas-sample-kafka-only.conf +++ /dev/null @@ -1,7 +0,0 @@ -KafkaClient { - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - keyTab="/etc/security/keytabs/kafka_client.keytab" - principal="kafka-client-1@EXAMPLE.COM"; -}; \ No newline at end of file diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/jaas-sample-with-zk.conf b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/jaas-sample-with-zk.conf deleted file mode 100644 index fe511bd62..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/jaas-sample-with-zk.conf +++ /dev/null @@ -1,14 +0,0 @@ -KafkaClient { - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - keyTab="/etc/security/keytabs/kafka_client.keytab" - principal="kafka-client-1@EXAMPLE.COM"; -}; -Client { - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - keyTab="/etc/security/keytabs/zk_client.keytab" - principal="zk-client-1@EXAMPLE.COM"; -}; \ No newline at end of file diff --git a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/logback.xml b/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/logback.xml deleted file mode 100644 index 77ab1d058..000000000 --- a/binders/kafka-binder/spring-cloud-stream-binder-kafka/src/test/resources/logback.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - %d{ISO8601} %5p %t %c{2}:%L - %m%n - - - - - - - - - - - diff --git a/binders/pom.xml b/binders/pom.xml deleted file mode 100644 index 5178cd931..000000000 --- a/binders/pom.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - 4.0.0 - spring-cloud-stream-binders - 4.1.0-SNAPSHOT - binders - Framework provided binders - pom - - - org.springframework.cloud - spring-cloud-stream-parent - 4.1.0-SNAPSHOT - - - - kafka-binder - rabbit-binder - pulsar-binder - - - diff --git a/binders/pulsar-binder/pom.xml b/binders/pulsar-binder/pom.xml deleted file mode 100644 index 61002d155..000000000 --- a/binders/pulsar-binder/pom.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - 4.0.0 - spring-cloud-stream-binder-pulsar-parent - 4.1.0-SNAPSHOT - pom - - org.springframework.cloud - spring-cloud-stream-binders - 4.1.0-SNAPSHOT - - - - 17 - true - true - true - - - spring-cloud-stream-binder-pulsar - - - - - org.junit.vintage - junit-vintage-engine - test - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M7 - - true - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - -parameters - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - - spring - - - - - coverage - - - env.TRAVIS - true - - - - - - org.jacoco - jacoco-maven-plugin - 0.7.9 - - - agent - - prepare-agent - - - - report - test - - report - - - - - - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - - spring-milestones - Spring milestones - https://repo.spring.io/libs-milestone-local - - - rsocket-snapshots - RSocket Snapshots - https://oss.jfrog.org/oss-snapshot-local - - true - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/pom.xml b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/pom.xml deleted file mode 100644 index 5f473cca7..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-binder-pulsar - jar - spring-cloud-stream-binder-pulsar - PUlsar binder implementation - - - org.springframework.cloud - spring-cloud-stream-binder-pulsar-parent - 4.1.0-SNAPSHOT - - - - - org.springframework.pulsar - spring-pulsar-spring-boot-starter - 0.2.1-SNAPSHOT - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.cloud - spring-cloud-stream-test-support - test - - - org.testcontainers - pulsar - 1.17.6 - test - - - org.testcontainers - junit-jupiter - 1.17.6 - test - - - org.awaitility - awaitility - test - - - - diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderHeaderMapper.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderHeaderMapper.java deleted file mode 100644 index 32b1bc02e..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderHeaderMapper.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.Map; - -import org.apache.pulsar.client.api.Message; - -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.MessageHeaderAccessor; -import org.springframework.pulsar.support.header.PulsarHeaderMapper; - - -/** - * A delegating {@code PulsarHeaderMapper} that ensures the delegate mapper never includes - * internal binder specific headers during outbound mapping. - * - * @author Chris Bono - */ -class PulsarBinderHeaderMapper implements PulsarHeaderMapper { - - private final PulsarHeaderMapper delegate; - - /** - * Construct a mapper with the specified delegate. - * @param delegate the delegate mapper - */ - PulsarBinderHeaderMapper(PulsarHeaderMapper delegate) { - this.delegate = delegate; - } - - @Override - public Map toPulsarHeaders(MessageHeaders springHeaders) { - Map pulsarHeaders = this.delegate.toPulsarHeaders(springHeaders); - pulsarHeaders.remove(MessageHeaders.ID); - pulsarHeaders.remove(MessageHeaders.TIMESTAMP); - pulsarHeaders.remove(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); - pulsarHeaders.remove(BinderHeaders.NATIVE_HEADERS_PRESENT); - return pulsarHeaders; - } - - @Override - public MessageHeaders toSpringHeaders(Message pulsarMessage) { - var springHeaders = this.delegate.toSpringHeaders(pulsarMessage); - if (!springHeaders.isEmpty()) { - MessageHeaderAccessor mutableHeaders = new MessageHeaderAccessor(); - mutableHeaders.copyHeaders(springHeaders); - mutableHeaders.setHeader(BinderHeaders.NATIVE_HEADERS_PRESENT, Boolean.TRUE); - springHeaders = mutableHeaders.getMessageHeaders(); - } - return springHeaders; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderUtils.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderUtils.java deleted file mode 100644 index 229372d7c..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderUtils.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.core.log.LogAccessor; -import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties; -import org.springframework.pulsar.autoconfigure.ProducerConfigProperties; -import org.springframework.util.StringUtils; -import org.springframework.util.unit.DataSize; - -/** - * Binder utility methods. - * - * @author Soby Chacko - * @author Chris Bono - */ -final class PulsarBinderUtils { - - private static final LogAccessor LOGGER = new LogAccessor(PulsarBinderUtils.class); - - private static final String SUBSCRIPTION_NAME_FORMAT_STR = "%s-anon-subscription-%s"; - - private PulsarBinderUtils() { - } - - /** - * Gets the subscription name to use for the binder. - * @param consumerProps the pulsar consumer props - * @param consumerDestination the destination being subscribed to - * @return the subscription name from the consumer properties or a generated name in - * the format {@link #SUBSCRIPTION_NAME_FORMAT_STR} when the name is not set on the - * consumer properties - */ - static String subscriptionName(PulsarConsumerProperties consumerProps, ConsumerDestination consumerDestination) { - if (StringUtils.hasText(consumerProps.getSubscriptionName())) { - return consumerProps.getSubscriptionName(); - } - return SUBSCRIPTION_NAME_FORMAT_STR.formatted(consumerDestination.getName(), UUID.randomUUID()); - } - - /** - * Merges properties defined at the binder and binding level (binding properties - * override binder properties). - *

- * NOTE: Properties whose value is not different from the default value in the - * {@code baseProps} are not included in the merged result. - * @param baseProps the map of base level properties (eg. 'spring.pulsar.consumer.*') - * @param binderProps the map of binder level properties (eg. - * 'spring.cloud.stream.pulsar.binder.consumer.*') - * @param bindingProps the map of binding level properties (eg. - * 'spring.cloud.stream.pulsar.bindings.myBinding-in-0.consumer.*') - * @return map of merged binder and binding properties including only properties whose - * value has changed from the same property in the base properties - */ - static Map mergePropertiesWithPrecedence(Map baseProps, - Map binderProps, Map bindingProps) { - Objects.requireNonNull(baseProps, "baseProps must be specified"); - Objects.requireNonNull(binderProps, "binderProps must be specified"); - Objects.requireNonNull(bindingProps, "bindingProps must be specified"); - - Map newOrModifiedBinderProps = extractNewOrModifiedProperties(binderProps, baseProps); - LOGGER.trace(() -> "New or modified binder props: %s".formatted(newOrModifiedBinderProps)); - - Map newOrModifiedBindingProps = extractNewOrModifiedProperties(bindingProps, baseProps); - LOGGER.trace(() -> "New or modified binding props: %s".formatted(newOrModifiedBindingProps)); - - Map mergedProps = new HashMap<>(newOrModifiedBinderProps); - mergedProps.putAll(newOrModifiedBindingProps); - LOGGER.trace(() -> "Final merged props: %s".formatted(mergedProps)); - - return mergedProps; - } - - private static Map extractNewOrModifiedProperties(Map candidateProps, - Map baseProps) { - Map newOrModifiedProps = new HashMap<>(); - candidateProps.forEach((propName, propValue) -> { - if (!baseProps.containsKey(propName) || (!Objects.equals(propValue, baseProps.get(propName)))) { - newOrModifiedProps.put(propName, propValue); - } - }); - return newOrModifiedProps; - } - - /** - * Gets a map representation of a {@link ProducerConfigProperties}. - * @param producerProps the producer props - * @return map representation of producer props where each entry is a field and its - * associated value - */ - static Map convertProducerPropertiesToMap(ProducerConfigProperties producerProps) { - var properties = new PulsarBinderUtils.Properties(); - var map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(producerProps::getTopicName).to(properties.in("topicName")); - map.from(producerProps::getProducerName).to(properties.in("producerName")); - map.from(producerProps::getSendTimeout).asInt(Duration::toMillis).to(properties.in("sendTimeoutMs")); - map.from(producerProps::getBlockIfQueueFull).to(properties.in("blockIfQueueFull")); - map.from(producerProps::getMaxPendingMessages).to(properties.in("maxPendingMessages")); - map.from(producerProps::getMaxPendingMessagesAcrossPartitions) - .to(properties.in("maxPendingMessagesAcrossPartitions")); - map.from(producerProps::getMessageRoutingMode).to(properties.in("messageRoutingMode")); - map.from(producerProps::getHashingScheme).to(properties.in("hashingScheme")); - map.from(producerProps::getCryptoFailureAction).to(properties.in("cryptoFailureAction")); - map.from(producerProps::getBatchingMaxPublishDelay).as(it -> it.toNanos() / 1000) - .to(properties.in("batchingMaxPublishDelayMicros")); - map.from(producerProps::getBatchingPartitionSwitchFrequencyByPublishDelay) - .to(properties.in("batchingPartitionSwitchFrequencyByPublishDelay")); - map.from(producerProps::getBatchingMaxMessages).to(properties.in("batchingMaxMessages")); - map.from(producerProps::getBatchingMaxBytes).asInt(DataSize::toBytes).to(properties.in("batchingMaxBytes")); - map.from(producerProps::getBatchingEnabled).to(properties.in("batchingEnabled")); - map.from(producerProps::getChunkingEnabled).to(properties.in("chunkingEnabled")); - map.from(producerProps::getEncryptionKeys).to(properties.in("encryptionKeys")); - map.from(producerProps::getCompressionType).to(properties.in("compressionType")); - map.from(producerProps::getInitialSequenceId).to(properties.in("initialSequenceId")); - map.from(producerProps::getAutoUpdatePartitions).to(properties.in("autoUpdatePartitions")); - map.from(producerProps::getAutoUpdatePartitionsInterval).as(Duration::toSeconds) - .to(properties.in("autoUpdatePartitionsIntervalSeconds")); - map.from(producerProps::getMultiSchema).to(properties.in("multiSchema")); - map.from(producerProps::getProducerAccessMode).to(properties.in("accessMode")); - map.from(producerProps::getLazyStartPartitionedProducers).to(properties.in("lazyStartPartitionedProducers")); - map.from(producerProps::getProperties).to(properties.in("properties")); - return properties; - } - - /** - * Gets a map representation of a {@link ConsumerConfigProperties}. - * @param consumerProps the consumer props - * @return map representation of consumer props where each entry is a field and its - * associated value - */ - static Map convertConsumerPropertiesToMap(ConsumerConfigProperties consumerProps) { - var properties = new PulsarBinderUtils.Properties(); - var map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(consumerProps::getTopics).to(properties.in("topicNames")); - map.from(consumerProps::getTopicsPattern).to(properties.in("topicsPattern")); - map.from(consumerProps::getSubscriptionName).to(properties.in("subscriptionName")); - map.from(consumerProps::getSubscriptionType).to(properties.in("subscriptionType")); - map.from(consumerProps::getSubscriptionProperties).to(properties.in("subscriptionProperties")); - map.from(consumerProps::getSubscriptionMode).to(properties.in("subscriptionMode")); - map.from(consumerProps::getReceiverQueueSize).to(properties.in("receiverQueueSize")); - map.from(consumerProps::getAcknowledgementsGroupTime).as(it -> it.toNanos() / 1000) - .to(properties.in("acknowledgementsGroupTimeMicros")); - map.from(consumerProps::getNegativeAckRedeliveryDelay).as(it -> it.toNanos() / 1000) - .to(properties.in("negativeAckRedeliveryDelayMicros")); - map.from(consumerProps::getMaxTotalReceiverQueueSizeAcrossPartitions) - .to(properties.in("maxTotalReceiverQueueSizeAcrossPartitions")); - map.from(consumerProps::getConsumerName).to(properties.in("consumerName")); - map.from(consumerProps::getAckTimeout).as(Duration::toMillis).to(properties.in("ackTimeoutMillis")); - map.from(consumerProps::getTickDuration).as(Duration::toMillis).to(properties.in("tickDurationMillis")); - map.from(consumerProps::getPriorityLevel).to(properties.in("priorityLevel")); - map.from(consumerProps::getCryptoFailureAction).to(properties.in("cryptoFailureAction")); - map.from(consumerProps::getProperties).to(properties.in("properties")); - map.from(consumerProps::getReadCompacted).to(properties.in("readCompacted")); - map.from(consumerProps::getSubscriptionInitialPosition).to(properties.in("subscriptionInitialPosition")); - map.from(consumerProps::getPatternAutoDiscoveryPeriod).to(properties.in("patternAutoDiscoveryPeriod")); - map.from(consumerProps::getRegexSubscriptionMode).to(properties.in("regexSubscriptionMode")); - map.from(consumerProps::getDeadLetterPolicy).to(properties.in("deadLetterPolicy")); - map.from(consumerProps::getRetryEnable).to(properties.in("retryEnable")); - map.from(consumerProps::getAutoUpdatePartitions).to(properties.in("autoUpdatePartitions")); - map.from(consumerProps::getAutoUpdatePartitionsInterval).as(Duration::toSeconds) - .to(properties.in("autoUpdatePartitionsIntervalSeconds")); - map.from(consumerProps::getReplicateSubscriptionState).to(properties.in("replicateSubscriptionState")); - map.from(consumerProps::getResetIncludeHead).to(properties.in("resetIncludeHead")); - map.from(consumerProps::getBatchIndexAckEnabled).to(properties.in("batchIndexAckEnabled")); - map.from(consumerProps::getAckReceiptEnabled).to(properties.in("ackReceiptEnabled")); - map.from(consumerProps::getPoolMessages).to(properties.in("poolMessages")); - map.from(consumerProps::getStartPaused).to(properties.in("startPaused")); - map.from(consumerProps::getAutoAckOldestChunkedMessageOnQueueFull) - .to(properties.in("autoAckOldestChunkedMessageOnQueueFull")); - map.from(consumerProps::getMaxPendingChunkedMessage).to(properties.in("maxPendingChunkedMessage")); - map.from(consumerProps::getExpireTimeOfIncompleteChunkedMessage).as(Duration::toMillis) - .to(properties.in("expireTimeOfIncompleteChunkedMessageMillis")); - return properties; - } - - static class Properties extends HashMap { - - java.util.function.Consumer in(String key) { - return (value) -> put(key, value); - } - - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarMessageChannelBinder.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarMessageChannelBinder.java deleted file mode 100644 index d6eafe2eb..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/PulsarMessageChannelBinder.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.Optional; -import java.util.Set; - -import org.apache.pulsar.client.api.PulsarClientException; -import org.apache.pulsar.client.api.Schema; -import org.apache.pulsar.common.schema.SchemaType; - -import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.HeaderMode; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarExtendedBindingProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarProducerProperties; -import org.springframework.cloud.stream.binder.pulsar.provisioning.PulsarTopicProvisioner; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.endpoint.MessageProducerSupport; -import org.springframework.integration.handler.AbstractMessageProducingHandler; -import org.springframework.integration.support.management.ManageableLifecycle; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties; -import org.springframework.pulsar.autoconfigure.ProducerConfigProperties; -import org.springframework.pulsar.core.ProducerBuilderConfigurationUtil; -import org.springframework.pulsar.core.ProducerBuilderCustomizer; -import org.springframework.pulsar.core.PulsarConsumerFactory; -import org.springframework.pulsar.core.PulsarTemplate; -import org.springframework.pulsar.core.SchemaResolver; -import org.springframework.pulsar.core.TypedMessageBuilderCustomizer; -import org.springframework.pulsar.listener.AbstractPulsarMessageListenerContainer; -import org.springframework.pulsar.listener.DefaultPulsarMessageListenerContainer; -import org.springframework.pulsar.listener.PulsarContainerProperties; -import org.springframework.pulsar.listener.PulsarRecordMessageListener; -import org.springframework.pulsar.support.header.PulsarHeaderMapper; - - -/** - * {@link Binder} implementation for Apache Pulsar. - * - * @author Soby Chacko - * @author Chris Bono - */ -public class PulsarMessageChannelBinder extends - AbstractMessageChannelBinder, ExtendedProducerProperties, PulsarTopicProvisioner> - implements ExtendedPropertiesBinder { - - private final PulsarTemplate pulsarTemplate; - - private final PulsarConsumerFactory pulsarConsumerFactory; - - private final PulsarBinderConfigurationProperties binderConfigProps; - - private final SchemaResolver schemaResolver; - - private final PulsarHeaderMapper headerMapper; - - private PulsarExtendedBindingProperties extendedBindingProperties = new PulsarExtendedBindingProperties(); - - public PulsarMessageChannelBinder(PulsarTopicProvisioner provisioningProvider, - PulsarTemplate pulsarTemplate, PulsarConsumerFactory pulsarConsumerFactory, - PulsarBinderConfigurationProperties binderConfigProps, SchemaResolver schemaResolver, - PulsarHeaderMapper headerMapper) { - super(null, provisioningProvider); - this.pulsarTemplate = pulsarTemplate; - this.pulsarConsumerFactory = pulsarConsumerFactory; - this.binderConfigProps = binderConfigProps; - this.schemaResolver = schemaResolver; - this.headerMapper = headerMapper; - } - - @Override - protected MessageHandler createProducerMessageHandler(ProducerDestination destination, - ExtendedProducerProperties producerProperties, MessageChannel errorChannel) { - final Schema schema; - if (producerProperties.isUseNativeEncoding()) { - var schemaType = Optional.ofNullable(producerProperties.getExtension().getSchemaType()) - .orElse(SchemaType.NONE); - schema = this.schemaResolver - .resolveSchema(schemaType, producerProperties.getExtension().getMessageType(), - producerProperties.getExtension().getMessageKeyType()) - .orElseThrow(() -> "Could not determine producer schema for " + destination.getName()); - } - else { - schema = null; - } - - var baseProducerProps = PulsarBinderUtils.convertProducerPropertiesToMap(new ProducerConfigProperties()); - var binderProducerProps = PulsarBinderUtils - .convertProducerPropertiesToMap(this.binderConfigProps.getProducer()); - var bindingProducerProps = PulsarBinderUtils.convertProducerPropertiesToMap(producerProperties.getExtension()); - var mergedProducerProps = PulsarBinderUtils.mergePropertiesWithPrecedence(baseProducerProps, - binderProducerProps, bindingProducerProps); - - var handler = new PulsarProducerConfigurationMessageHandler(this.pulsarTemplate, schema, destination.getName(), - (builder) -> ProducerBuilderConfigurationUtil.loadConf(builder, mergedProducerProps), - determineOutboundHeaderMapper(producerProperties)); - handler.setApplicationContext(getApplicationContext()); - handler.setBeanFactory(getBeanFactory()); - - return handler; - } - - @Nullable - private PulsarBinderHeaderMapper determineOutboundHeaderMapper( - ExtendedProducerProperties extProducerProps) { - if (HeaderMode.none.equals(extProducerProps.getHeaderMode())) { - return null; - } - return new PulsarBinderHeaderMapper(this.headerMapper); - } - - @Override - protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group, - ExtendedConsumerProperties properties) { - var containerProperties = new PulsarContainerProperties(); - containerProperties.setTopics(Set.of(destination.getName())); - - var inboundHeaderMapper = determineInboundHeaderMapper(properties); - - var messageDrivenChannelAdapter = new PulsarMessageDrivenChannelAdapter(); - containerProperties.setMessageListener((PulsarRecordMessageListener) (consumer, pulsarMsg) -> { - var springMessage = (inboundHeaderMapper != null) - ? MessageBuilder.createMessage(pulsarMsg.getValue(), inboundHeaderMapper.toSpringHeaders(pulsarMsg)) - : MessageBuilder.withPayload(pulsarMsg.getValue()).build(); - messageDrivenChannelAdapter.send(springMessage); - }); - - if (properties.isUseNativeDecoding()) { - var schemaType = Optional.ofNullable(properties.getExtension().getSchemaType()).orElse(SchemaType.NONE); - var schema = this.schemaResolver - .resolveSchema(schemaType, properties.getExtension().getMessageType(), - properties.getExtension().getMessageKeyType()) - .orElseThrow(() -> "Could not determine consumer schema for " + destination.getName()); - containerProperties.setSchema(schema); - } - else { - containerProperties.setSchema(Schema.BYTES); - } - var subscriptionName = PulsarBinderUtils.subscriptionName(properties.getExtension(), destination); - containerProperties.setSubscriptionName(subscriptionName); - - var baseConsumerProps = PulsarBinderUtils.convertConsumerPropertiesToMap(new ConsumerConfigProperties()); - var binderConsumerProps = PulsarBinderUtils - .convertConsumerPropertiesToMap(this.binderConfigProps.getConsumer()); - var bindingConsumerProps = PulsarBinderUtils.convertConsumerPropertiesToMap(properties.getExtension()); - var mergedConsumerProps = PulsarBinderUtils.mergePropertiesWithPrecedence(baseConsumerProps, - binderConsumerProps, bindingConsumerProps); - containerProperties.getPulsarConsumerProperties().putAll(mergedConsumerProps); - containerProperties.updateContainerProperties(); - - var container = new DefaultPulsarMessageListenerContainer<>(this.pulsarConsumerFactory, containerProperties); - messageDrivenChannelAdapter.setMessageListenerContainer(container); - - return messageDrivenChannelAdapter; - } - - @Nullable - private PulsarBinderHeaderMapper determineInboundHeaderMapper( - ExtendedConsumerProperties extConsumerProps) { - if (HeaderMode.none.equals(extConsumerProps.getHeaderMode())) { - return null; - } - return new PulsarBinderHeaderMapper(this.headerMapper); - } - - @Override - public PulsarConsumerProperties getExtendedConsumerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedConsumerProperties(channelName); - } - - @Override - public PulsarProducerProperties getExtendedProducerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedProducerProperties(channelName); - } - - @Override - public String getDefaultsPrefix() { - return null; - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return null; - } - - public PulsarExtendedBindingProperties getExtendedBindingProperties() { - return this.extendedBindingProperties; - } - - public void setExtendedBindingProperties(PulsarExtendedBindingProperties extendedBindingProperties) { - this.extendedBindingProperties = extendedBindingProperties; - } - - static class PulsarMessageDrivenChannelAdapter extends MessageProducerSupport { - - AbstractPulsarMessageListenerContainer messageListenerContainer; - - public void send(Message message) { - sendMessage(message); - } - - @Override - protected void doStart() { - this.messageListenerContainer.start(); - } - - @Override - protected void doStop() { - this.messageListenerContainer.stop(); - } - - public void setMessageListenerContainer(AbstractPulsarMessageListenerContainer messageListenerContainer) { - this.messageListenerContainer = messageListenerContainer; - } - - } - - static class PulsarProducerConfigurationMessageHandler extends AbstractMessageProducingHandler - implements ManageableLifecycle { - - private final PulsarTemplate pulsarTemplate; - - private final Schema schema; - - private final String destination; - - private final ProducerBuilderCustomizer layeredProducerPropsCustomizer; - - private final PulsarHeaderMapper headerMapper; - - private boolean running = true; - - PulsarProducerConfigurationMessageHandler(PulsarTemplate pulsarTemplate, Schema schema, - String destination, ProducerBuilderCustomizer layeredProducerPropsCustomizer, - PulsarHeaderMapper headerMapper) { - this.pulsarTemplate = pulsarTemplate; - this.schema = schema; - this.destination = destination; - this.layeredProducerPropsCustomizer = layeredProducerPropsCustomizer; - this.headerMapper = headerMapper; - } - - @Override - public void start() { - try { - super.onInit(); - } - catch (Exception ex) { - this.logger.error(ex, "Initialization errors: "); - throw new RuntimeException(ex); - } - } - - @Override - public void stop() { - // TODO - should we close the underlyiung producer? - this.running = false; - } - - @Override - public boolean isRunning() { - return this.running; - } - - @Override - protected void handleMessageInternal(Message message) { - try { - // @formatter:off - this.pulsarTemplate.newMessage(message.getPayload()) - .withTopic(this.destination) - .withSchema(this.schema) - .withProducerCustomizer(this.layeredProducerPropsCustomizer) - .withMessageCustomizer(this.applySpringHeadersAsPulsarProperties(message.getHeaders())) - .sendAsync(); - // @formatter:on - } - catch (PulsarClientException ex) { - logger.trace(ex, "Failed to send message to destination: " + this.destination); - } - } - - private TypedMessageBuilderCustomizer applySpringHeadersAsPulsarProperties(MessageHeaders headers) { - return (mb) -> { - if (this.headerMapper != null) { - this.headerMapper.toPulsarHeaders(headers).forEach(mb::property); - } - }; - } - - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/config/PulsarBinderConfiguration.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/config/PulsarBinderConfiguration.java deleted file mode 100644 index 6e77e06a6..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/config/PulsarBinderConfiguration.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.config; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.pulsar.PulsarMessageChannelBinder; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarExtendedBindingProperties; -import org.springframework.cloud.stream.binder.pulsar.provisioning.PulsarTopicProvisioner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.pulsar.autoconfigure.PulsarProperties; -import org.springframework.pulsar.core.PulsarAdministration; -import org.springframework.pulsar.core.PulsarConsumerFactory; -import org.springframework.pulsar.core.PulsarTemplate; -import org.springframework.pulsar.core.SchemaResolver; -import org.springframework.pulsar.support.header.JacksonUtils; -import org.springframework.pulsar.support.header.JsonPulsarHeaderMapper; -import org.springframework.pulsar.support.header.PulsarHeaderMapper; -import org.springframework.pulsar.support.header.ToStringPulsarHeaderMapper; - -/** - * Pulsar binder {@link Configuration}. - * - * @author Soby Chacko - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(Binder.class) -@EnableConfigurationProperties({ PulsarProperties.class, PulsarExtendedBindingProperties.class, - PulsarBinderConfigurationProperties.class }) -public class PulsarBinderConfiguration { - - @Bean - public PulsarTopicProvisioner pulsarTopicProvisioner(PulsarAdministration pulsarAdministration, - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties) { - return new PulsarTopicProvisioner(pulsarAdministration, pulsarBinderConfigurationProperties); - } - - @Bean - @ConditionalOnMissingBean - public PulsarHeaderMapper pulsarHeaderMapper() { - if (JacksonUtils.isJacksonPresent()) { - return JsonPulsarHeaderMapper.builder().build(); - } - return new ToStringPulsarHeaderMapper(); - } - - @Bean - public PulsarMessageChannelBinder pulsarMessageChannelBinder(PulsarTopicProvisioner pulsarTopicProvisioner, - PulsarTemplate pulsarTemplate, PulsarConsumerFactory pulsarConsumerFactory, - PulsarBinderConfigurationProperties binderConfigProps, PulsarExtendedBindingProperties bindingConfigProps, - SchemaResolver schemaResolver, PulsarHeaderMapper headerMapper) { - PulsarMessageChannelBinder pulsarMessageChannelBinder = new PulsarMessageChannelBinder(pulsarTopicProvisioner, - pulsarTemplate, pulsarConsumerFactory, binderConfigProps, schemaResolver, headerMapper); - pulsarMessageChannelBinder.setExtendedBindingProperties(bindingConfigProps); - return pulsarMessageChannelBinder; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/package-info.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/package-info.java deleted file mode 100644 index 95966720b..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Package containing Spring Cloud Stream binder classes for Apache Pulsar. - */ -@NonNullApi -@NonNullFields -package org.springframework.cloud.stream.binder.pulsar; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarBinderConfigurationProperties.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarBinderConfigurationProperties.java deleted file mode 100644 index 7722b7c6e..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarBinderConfigurationProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.lang.Nullable; -import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties; -import org.springframework.pulsar.autoconfigure.ProducerConfigProperties; - -/** - * {@link ConfigurationProperties @ConfigurationProperties} for the Pulsar binder. - *

- * These properties are applied at the binder level (to all bindings). - * - * @author Soby Chacko - * @author Chris Bono - */ -@ConfigurationProperties(prefix = "spring.cloud.stream.pulsar.binder") -public class PulsarBinderConfigurationProperties { - - /** - * Pulsar consumer specific binder-level properties (applied to all bindings). - */ - @NestedConfigurationProperty - private final ConsumerConfigProperties consumer = new ConsumerConfigProperties(); - - /** - * Pulsar producer specific binder-level properties (applied to all bindings). - */ - @NestedConfigurationProperty - private final ProducerConfigProperties producer = new ProducerConfigProperties(); - - /** - * Number of topic partitions. - */ - @Nullable - private Integer partitionCount; - - public ConsumerConfigProperties getConsumer() { - return this.consumer; - } - - public ProducerConfigProperties getProducer() { - return this.producer; - } - - @Nullable - public Integer getPartitionCount() { - return this.partitionCount; - } - - public void setPartitionCount(Integer partitionCount) { - this.partitionCount = partitionCount; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarBindingProperties.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarBindingProperties.java deleted file mode 100644 index 8abe593ca..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarBindingProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * Container for Pulsar specific extended producer and consumer binding properties. - *

- * These properties are applied to individual bindings and will override any binder-level - * setting. - * - *

- * NOTE: This class is only referenced as a value in the - * {@link PulsarExtendedBindingProperties#getBindings() bindings map} and therefore, by - * default is not included in the generated configuration metadata. To get around this - * limitation it is annotated with {@code @ConfigurationProperties}. However, that is the - * only reason it is annotated and is not intended to be used directly. - * - * @author Soby Chacko - * @author Chris Bono - */ -@SuppressWarnings("ConfigurationProperties") -@ConfigurationProperties("spring.cloud.stream.pulsar.bindings.for-docs-only") -public class PulsarBindingProperties implements BinderSpecificPropertiesProvider { - - /** - * Pulsar consumer specific binding properties. - */ - @NestedConfigurationProperty - private PulsarConsumerProperties consumer = new PulsarConsumerProperties(); - - /** - * Pulsar producer specific binding properties. - */ - @NestedConfigurationProperty - private PulsarProducerProperties producer = new PulsarProducerProperties(); - - public PulsarConsumerProperties getConsumer() { - return this.consumer; - } - - public void setConsumer(PulsarConsumerProperties consumer) { - this.consumer = consumer; - } - - public PulsarProducerProperties getProducer() { - return this.producer; - } - - public void setProducer(PulsarProducerProperties producer) { - this.producer = producer; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarConsumerProperties.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarConsumerProperties.java deleted file mode 100644 index ea4684aac..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarConsumerProperties.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.properties; - -import org.apache.pulsar.common.schema.SchemaType; - -import org.springframework.lang.Nullable; -import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties; - -/** - * Pulsar consumer properties used by the binder. - * - * @author Soby Chacko - * @author Chris Bono - */ -public class PulsarConsumerProperties extends ConsumerConfigProperties { - - /** - * Pulsar {@link SchemaType} for this binding. - */ - @Nullable - private SchemaType schemaType; - - /** - * Pulsar message type for this binding. - */ - @Nullable - private Class messageType; - - /** - * Pulsar message key type for this binding (only used when schema type is - * {@code }KEY_VALUE}). - */ - @Nullable - private Class messageKeyType; - - /** - * Number of topic partitions. - */ - @Nullable - private Integer partitionCount; - - @Nullable - public SchemaType getSchemaType() { - return this.schemaType; - } - - public void setSchemaType(@Nullable SchemaType schemaType) { - this.schemaType = schemaType; - } - - @Nullable - public Class getMessageType() { - return this.messageType; - } - - public void setMessageType(@Nullable Class messageType) { - this.messageType = messageType; - } - - @Nullable - public Class getMessageKeyType() { - return this.messageKeyType; - } - - public void setMessageKeyType(@Nullable Class messageKeyType) { - this.messageKeyType = messageKeyType; - } - - @Nullable - public Integer getPartitionCount() { - return this.partitionCount; - } - - public void setPartitionCount(Integer partitionCount) { - this.partitionCount = partitionCount; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarExtendedBindingProperties.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarExtendedBindingProperties.java deleted file mode 100644 index 4e487a3c0..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarExtendedBindingProperties.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.properties; - -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - - -/** - * {@link ConfigurationProperties @ConfigurationProperties} for Pulsar binder specific - * extensions to the common binding properties. - *

- * These properties are applied to individual bindings and will override any binder-level - * settings. - * - * @author Soby Chacko - * @author Chris Bono - */ -@ConfigurationProperties("spring.cloud.stream.pulsar") -public class PulsarExtendedBindingProperties extends - AbstractExtendedBindingProperties { - - private static final String DEFAULTS_PREFIX = "spring.cloud.stream.pulsar.default"; - - @Override - public String getDefaultsPrefix() { - return DEFAULTS_PREFIX; - } - - /** - * Properties per individual binding name (e.g. 'mySink-in-0'). - */ - @Override - public Map getBindings() { - return this.doGetBindings(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return PulsarBindingProperties.class; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarProducerProperties.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarProducerProperties.java deleted file mode 100644 index db73378e2..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/PulsarProducerProperties.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.properties; - -import org.apache.pulsar.common.schema.SchemaType; - -import org.springframework.lang.Nullable; -import org.springframework.pulsar.autoconfigure.ProducerConfigProperties; - -/** - * Pulsar producer properties used by the binder. - * - * @author Soby Chacko - * @author Chris Bono - */ -public class PulsarProducerProperties extends ProducerConfigProperties { - - /** - * Pulsar {@link SchemaType} for this binding. - */ - @Nullable - private SchemaType schemaType; - - /** - * Pulsar message type for this binding. - */ - @Nullable - private Class messageType; - - /** - * Pulsar message key type for this binding (only used when schema type is - * {@code }KEY_VALUE}). - */ - @Nullable - private Class messageKeyType; - - /** - * Number of topic partitions. - */ - @Nullable - private Integer partitionCount; - - @Nullable - public SchemaType getSchemaType() { - return this.schemaType; - } - - public void setSchemaType(@Nullable SchemaType schemaType) { - this.schemaType = schemaType; - } - - @Nullable - public Class getMessageType() { - return this.messageType; - } - - public void setMessageType(@Nullable Class messageType) { - this.messageType = messageType; - } - - @Nullable - public Class getMessageKeyType() { - return this.messageKeyType; - } - - public void setMessageKeyType(@Nullable Class messageKeyType) { - this.messageKeyType = messageKeyType; - } - - @Nullable - public Integer getPartitionCount() { - return this.partitionCount; - } - - public void setPartitionCount(Integer partitionCount) { - this.partitionCount = partitionCount; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/package-info.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/package-info.java deleted file mode 100644 index 688bc2473..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/properties/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Package containing Spring Cloud Stream binder properties classes for Apache Pulsar. - */ -@NonNullApi -@NonNullFields -package org.springframework.cloud.stream.binder.pulsar.properties; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/provisioning/PulsarTopicProvisioner.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/provisioning/PulsarTopicProvisioner.java deleted file mode 100644 index 46a2f674e..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/provisioning/PulsarTopicProvisioner.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar.provisioning; - -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarProducerProperties; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.cloud.stream.provisioning.ProvisioningException; -import org.springframework.cloud.stream.provisioning.ProvisioningProvider; -import org.springframework.lang.Nullable; -import org.springframework.pulsar.core.PulsarAdministration; -import org.springframework.pulsar.core.PulsarTopic; - -/** - * Pulsar topic provisioner. - * - * @author Soby Chacko - */ -public class PulsarTopicProvisioner implements - ProvisioningProvider, ExtendedProducerProperties> { - - private final PulsarAdministration pulsarAdministration; - - private final PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties; - - public PulsarTopicProvisioner(PulsarAdministration pulsarAdministration, - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties) { - this.pulsarAdministration = pulsarAdministration; - this.pulsarBinderConfigurationProperties = pulsarBinderConfigurationProperties; - } - - @Override - public ProducerDestination provisionProducerDestination(String name, - ExtendedProducerProperties pulsarProducerProperties) - throws ProvisioningException { - Integer partitionCountFromBinding = pulsarProducerProperties.getExtension().getPartitionCount(); - var partitionCount = getPartitionCount(partitionCountFromBinding); - var pulsarTopic = PulsarTopic.builder(name).numberOfPartitions(partitionCount).build(); - this.pulsarAdministration.createOrModifyTopics(pulsarTopic); - return new PulsarDestination(pulsarTopic.topicName(), pulsarTopic.numberOfPartitions()); - } - - private int getPartitionCount(@Nullable Integer partitionCountConfig) { - var partitionCount = this.pulsarBinderConfigurationProperties.getPartitionCount(); - if (partitionCountConfig != null && partitionCountConfig > 0) { - partitionCount = partitionCountConfig; - } - return partitionCount == null ? 0 : partitionCount; - } - - @Override - public ConsumerDestination provisionConsumerDestination(String name, String group, - ExtendedConsumerProperties pulsarConsumerProperties) - throws ProvisioningException { - var partitionCountFromBinding = pulsarConsumerProperties.getExtension().getPartitionCount(); - var partitionCount = getPartitionCount(partitionCountFromBinding); - var pulsarTopic = PulsarTopic.builder(name).numberOfPartitions(partitionCount).build(); - this.pulsarAdministration.createOrModifyTopics(pulsarTopic); - return new PulsarDestination(pulsarTopic.topicName(), pulsarTopic.numberOfPartitions()); - } - - private record PulsarDestination(String destinationName, - Integer partitions) implements ProducerDestination, ConsumerDestination { - - @Override - public String getName() { - return this.destinationName; - } - - @Override - public String getNameForPartition(int partition) { - return this.destinationName; - } - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/provisioning/package-info.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/provisioning/package-info.java deleted file mode 100644 index c6a016779..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/java/org/springframework/cloud/stream/binder/pulsar/provisioning/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Package containing Spring Cloud Stream binder provisioning classes for Apache Pulsar. - */ -@NonNullApi -@NonNullFields -package org.springframework.cloud.stream.binder.pulsar.provisioning; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/resources/META-INF/spring.binders b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/resources/META-INF/spring.binders deleted file mode 100644 index 778e113a6..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -pulsar:\ -org.springframework.cloud.stream.binder.pulsar.config.PulsarBinderConfiguration diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/AbstractPulsarTestBinder.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/AbstractPulsarTestBinder.java deleted file mode 100644 index a6191c2f7..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/AbstractPulsarTestBinder.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import org.springframework.cloud.stream.binder.AbstractPollableConsumerTestBinder; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarProducerProperties; -import org.springframework.context.ApplicationContext; - -/** - * Base class for {@link PulsarTestBinder}. - * - * @author Soby Chacko - */ -public abstract class AbstractPulsarTestBinder extends - AbstractPollableConsumerTestBinder, ExtendedProducerProperties> { - - private ApplicationContext applicationContext; - - @Override - public void cleanup() { - } - - protected final void setApplicationContext(ApplicationContext context) { - this.applicationContext = context; - } - - public ApplicationContext getApplicationContext() { - return this.applicationContext; - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderConfigurationPropertiesTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderConfigurationPropertiesTests.java deleted file mode 100644 index d21b40335..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderConfigurationPropertiesTests.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.pulsar.client.api.ProducerAccessMode; -import org.apache.pulsar.client.api.SubscriptionMode; -import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils; -import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData; -import org.apache.pulsar.client.impl.conf.ProducerConfigurationData; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; - -/** - * Tests for {@link PulsarBinderConfigurationProperties}. - * - * @author Chris Bono - */ -public class PulsarBinderConfigurationPropertiesTests { - - private final PulsarBinderConfigurationProperties properties = new PulsarBinderConfigurationProperties(); - - private void bind(Map map) { - ConfigurationPropertySource source = new MapConfigurationPropertySource(map); - new Binder(source).bind("spring.cloud.stream.pulsar.binder", Bindable.ofInstance(this.properties)); - } - - @Test - void partitionCountProperty() { - assertThat(properties.getPartitionCount()).isNull(); - bind(Map.of("spring.cloud.stream.pulsar.binder.partition-count", "5150")); - assertThat(properties.getPartitionCount()).isEqualTo(5150); - } - - @Test - void producerProperties() { - // Only spot check a few values (PulsarPropertiesTests does the heavy lifting) - Map props = new HashMap<>(); - props.put("spring.cloud.stream.pulsar.binder.producer.topic-name", "my-topic"); - props.put("spring.cloud.stream.pulsar.binder.producer.send-timeout", "2s"); - props.put("spring.cloud.stream.pulsar.binder.producer.max-pending-messages", "3"); - props.put("spring.cloud.stream.pulsar.binder.producer.producer-access-mode", "exclusive"); - props.put("spring.cloud.stream.pulsar.binder.producer.properties[my-prop]", "my-prop-value"); - - bind(props); - Map producerProps = PulsarBinderUtils.convertProducerPropertiesToMap(properties.getProducer()); - - // Verify that the props can be loaded in a ProducerBuilder - assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(producerProps, - new ProducerConfigurationData(), ProducerConfigurationData.class)); - - // @formatter:off - assertThat(producerProps) - .containsEntry("topicName", "my-topic") - .containsEntry("sendTimeoutMs", 2_000) - .containsEntry("maxPendingMessages", 3) - .containsEntry("accessMode", ProducerAccessMode.Exclusive) - .hasEntrySatisfying("properties", properties -> - assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-prop", "my-prop-value")); - // @formatter:on - } - - @Test - void consumerProperties() { - // Only spot check a few values (PulsarPropertiesTests does the heavy lifting) - Map props = new HashMap<>(); - props.put("spring.cloud.stream.pulsar.binder.consumer.topics[0]", "my-topic"); - props.put("spring.cloud.stream.pulsar.binder.consumer.subscription-properties[my-sub-prop]", - "my-sub-prop-value"); - props.put("spring.cloud.stream.pulsar.binder.consumer.subscription-mode", "nondurable"); - props.put("spring.cloud.stream.pulsar.binder.consumer.receiver-queue-size", "1"); - - bind(props); - Map consumerProps = PulsarBinderUtils.convertConsumerPropertiesToMap(properties.getConsumer()); - - // Verify that the props can be loaded in a ConsumerBuilder - assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(consumerProps, - new ConsumerConfigurationData<>(), ConsumerConfigurationData.class)); - - // @formatter:off - assertThat(consumerProps) - .hasEntrySatisfying("topicNames", - topics -> assertThat(topics).asInstanceOf(InstanceOfAssertFactories.collection(String.class)) - .containsExactly("my-topic")) - .hasEntrySatisfying("subscriptionProperties", - properties -> assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-sub-prop", "my-sub-prop-value")) - .containsEntry("subscriptionMode", SubscriptionMode.NonDurable) - .containsEntry("receiverQueueSize", 1); - // @formatter:on - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderHeaderMapperTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderHeaderMapperTests.java deleted file mode 100644 index 6a2b5f1cf..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderHeaderMapperTests.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.pulsar.client.api.Message; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.messaging.MessageHeaders; -import org.springframework.pulsar.support.header.PulsarHeaderMapper; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.entry; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -/** - * Tests for {@link PulsarBinderHeaderMapper}. - * - * @author Chris Bono - */ -@ExtendWith(MockitoExtension.class) -class PulsarBinderHeaderMapperTests { - - @Mock - private PulsarHeaderMapper delegateMapper; - - @InjectMocks - private PulsarBinderHeaderMapper binderHeaderMapper; - - @Nested - class ToPulsarHeadersOutboundTests { - - @Test - void delegateReturnsEmptyHeaders() { - var delegatePulsarHeaders = new HashMap(); - when(delegateMapper.toPulsarHeaders(any(MessageHeaders.class))).thenReturn(delegatePulsarHeaders); - var springHeaders = mock(MessageHeaders.class); - var pulsarHeaders = binderHeaderMapper.toPulsarHeaders(springHeaders); - verify(delegateMapper).toPulsarHeaders(springHeaders); - assertThat(pulsarHeaders).isEmpty(); - } - - @Test - void neverHeadersRemovedFromDelegateHeaders() { - var delegatePulsarHeaders = new HashMap(); - delegatePulsarHeaders.put(MessageHeaders.ID, "5150"); - delegatePulsarHeaders.put(MessageHeaders.TIMESTAMP, "12345"); - delegatePulsarHeaders.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, "5"); - delegatePulsarHeaders.put(BinderHeaders.NATIVE_HEADERS_PRESENT, "true"); - delegatePulsarHeaders.put("foo", "bar"); - when(delegateMapper.toPulsarHeaders(any(MessageHeaders.class))).thenReturn(delegatePulsarHeaders); - var springHeaders = mock(MessageHeaders.class); - var pulsarHeaders = binderHeaderMapper.toPulsarHeaders(springHeaders); - verify(delegateMapper).toPulsarHeaders(springHeaders); - assertThat(pulsarHeaders).containsOnly(entry("foo", "bar")); - } - - } - - @Nested - class ToSpringHeadersInboundTests { - - @Test - void delegateReturnsEmptyHeaders() { - var emptyDelegateHeaders = mock(MessageHeaders.class); - when(emptyDelegateHeaders.isEmpty()).thenReturn(true); - when(delegateMapper.toSpringHeaders(any(Message.class))).thenReturn(emptyDelegateHeaders); - var springHeaders = binderHeaderMapper.toSpringHeaders(mock(Message.class)); - assertThat(springHeaders).isSameAs(emptyDelegateHeaders); - verify(springHeaders).isEmpty(); - verifyNoMoreInteractions(springHeaders); - } - - @Test - void nativeHeadersIndicatorAddedToDelegateHeaders() { - var delegateSpringHeaders = new MessageHeaders(Map.of("foo", "bar")); - when(delegateMapper.toSpringHeaders(any(Message.class))).thenReturn(delegateSpringHeaders); - var pulsarMessage = mock(Message.class); - var springHeaders = binderHeaderMapper.toSpringHeaders(pulsarMessage); - verify(delegateMapper).toSpringHeaders(pulsarMessage); - assertThat(springHeaders).containsEntry("foo", "bar").containsEntry(BinderHeaders.NATIVE_HEADERS_PRESENT, - Boolean.TRUE); - - } - - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderIntegrationTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderIntegrationTests.java deleted file mode 100644 index 08ec2731b..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderIntegrationTests.java +++ /dev/null @@ -1,892 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.apache.pulsar.client.api.Producer; -import org.apache.pulsar.client.api.PulsarClient; -import org.apache.pulsar.client.api.PulsarClientException; -import org.apache.pulsar.client.api.Schema; -import org.apache.pulsar.client.impl.schema.JSONSchema; -import org.apache.pulsar.common.schema.KeyValue; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.pulsar.autoconfigure.PulsarProperties; -import org.springframework.pulsar.core.ConsumerBuilderCustomizer; -import org.springframework.pulsar.core.DefaultPulsarConsumerFactory; -import org.springframework.pulsar.core.DefaultPulsarProducerFactory; -import org.springframework.pulsar.core.DefaultSchemaResolver; -import org.springframework.pulsar.core.ProducerBuilderCustomizer; -import org.springframework.pulsar.core.PulsarConsumerFactory; -import org.springframework.pulsar.core.PulsarProducerFactory; -import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer; -import org.springframework.pulsar.core.TopicResolver; -import org.springframework.pulsar.support.header.PulsarHeaderMapper; -import org.springframework.pulsar.support.header.ToStringPulsarHeaderMapper; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for {@link PulsarBinderIntegrationTests}. - * - * @author Soby Chacko - * @author Chris Bono - */ -@ExtendWith(OutputCaptureExtension.class) -@SuppressWarnings("JUnitMalformedDeclaration") -class PulsarBinderIntegrationTests implements PulsarTestContainerSupport { - - private static final int AWAIT_DURATION = 10; - - @Test - void binderAndBindingPropsAreAppliedAndRespected(CapturedOutput output) { - SpringApplication app = new SpringApplication(BinderAndBindingPropsTestConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext context = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=textSupplier;textLogger", - "--spring.cloud.stream.bindings.textLogger-in-0.destination=textSupplier-out-0", - "--spring.pulsar.producer.producer-name=textSupplierProducer-fromBase", - "--spring.cloud.stream.pulsar.binder.producer.producer-name=textSupplierProducer-fromBinder", - "--spring.cloud.stream.pulsar.bindings.textSupplier-out-0.producer.producer-name=textSupplierProducer-fromBinding", - "--spring.cloud.stream.pulsar.binder.producer.max-pending-messages=1100", - "--spring.pulsar.producer.block-if-queue-full=true", - "--spring.cloud.stream.pulsar.binder.consumer.subscription-name=textLoggerSub-fromBinder", - "--spring.cloud.stream.pulsar.binder.consumer.consumer-name=textLogger-fromBinder", - "--spring.cloud.stream.pulsar.bindings.textLogger-in-0.consumer.consumer-name=textLogger-fromBinding")) { - - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: test-basic-scenario")); - - // now verify the properties were set onto producer and consumer as expected - TrackingProducerFactory producerFactory = context.getBean(TrackingProducerFactory.class); - assertThat(producerFactory.producersCreated).isNotEmpty().element(0) - .hasFieldOrPropertyWithValue("producerName", "textSupplierProducer-fromBinding") - .hasFieldOrPropertyWithValue("conf.maxPendingMessages", 1100) - .hasFieldOrPropertyWithValue("conf.blockIfQueueFull", true); - - TrackingConsumerFactory consumerFactory = context.getBean(TrackingConsumerFactory.class); - assertThat(consumerFactory.consumersCreated).isNotEmpty().element(0) - .hasFieldOrPropertyWithValue("consumerName", "textLogger-fromBinding") - .hasFieldOrPropertyWithValue("conf.subscriptionName", "textLoggerSub-fromBinder"); - } - } - - @Nested - class DefaultEncoding { - - @Test - void primitiveTypeString(CapturedOutput output) { - SpringApplication app = new SpringApplication(PrimitiveTextConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=textSupplier;textLogger", - "--spring.cloud.stream.bindings.textLogger-in-0.destination=textSupplier-out-0", - "--spring.cloud.stream.pulsar.bindings.textLogger-in-0.consumer.subscription-name=pbit-text-sub1")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: test-basic-scenario")); - } - } - - @Test - void primitiveTypeFloat(CapturedOutput output) { - SpringApplication app = new SpringApplication(PrimitiveFloatConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=piSupplier;piLogger", - "--spring.cloud.stream.bindings.piSupplier-out-0.destination=pi-stream", - "--spring.cloud.stream.bindings.piLogger-in-0.destination=pi-stream", - "--spring.cloud.stream.pulsar.bindings.piLogger-in-0.consumer.subscription-name=pbit-float-sub1")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: 3.14")); - } - } - - } - - @Nested - class NativeEncoding { - - @Test - void primitiveTypeFloat(CapturedOutput output) { - SpringApplication app = new SpringApplication(PrimitiveFloatConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=piSupplier;piLogger", - "--spring.cloud.stream.bindings.piLogger-in-0.destination=piSupplier-out-0", - "--spring.cloud.stream.bindings.piSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.piSupplier-out-0.producer.schema-type=FLOAT", - "--spring.cloud.stream.bindings.piLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.piLogger-in-0.consumer.schema-type=FLOAT", - "--spring.cloud.stream.pulsar.bindings.piLogger-in-0.consumer.subscription-name=pbit-float-sub2")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: 3.14")); - } - } - - @Test - void jsonTypeFooWithSchemaType(CapturedOutput output) { - SpringApplication app = new SpringApplication(JsonFooConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=fooSupplier;fooLogger", - "--spring.cloud.stream.bindings.fooSupplier-out-0.destination=foo-stream-1", - "--spring.cloud.stream.bindings.fooLogger-in-0.destination=foo-stream-1", - "--spring.cloud.stream.bindings.fooSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.schema-type=JSON", - "--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-type=" - + Foo.class.getName(), - "--spring.cloud.stream.bindings.fooLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.schema-type=JSON", - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-type=" + Foo.class.getName(), - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.subscription-name=pbit-foo-sub1")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: Foo[value=5150]")); - } - } - - @Test - void jsonTypeFooWithoutSchemaTypeDefaultsToJsonSchema(CapturedOutput output) { - SpringApplication app = new SpringApplication(JsonFooConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=fooSupplier;fooLogger", - "--spring.cloud.stream.bindings.fooSupplier-out-0.destination=foo-stream-2", - "--spring.cloud.stream.bindings.fooLogger-in-0.destination=foo-stream-2", - "--spring.cloud.stream.bindings.fooSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-type=" - + Foo.class.getName(), - "--spring.cloud.stream.bindings.fooLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-type=" + Foo.class.getName(), - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.subscription-name=pbit-foo-sub2")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: Foo[value=5150]")); - } - } - - @Test - void avroTypeUserWithSchemaType(CapturedOutput output) { - SpringApplication app = new SpringApplication(AvroUserConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=userSupplier;userLogger", - "--spring.cloud.stream.bindings.userSupplier-out-0.destination=user-stream-1", - "--spring.cloud.stream.bindings.userLogger-in-0.destination=user-stream-1", - "--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.schema-type=AVRO", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.schema-type=AVRO", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-user-sub1")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: User{name='user21', age=21}")); - } - } - - @Test - void avroTypeUserWithoutSchemaTypeWithCustomMappingsViaProps(CapturedOutput output) { - SpringApplication app = new SpringApplication(AvroUserConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=userSupplier;userLogger", - "--spring.cloud.stream.bindings.userSupplier-out-0.destination=user-stream-2", - "--spring.cloud.stream.bindings.userLogger-in-0.destination=user-stream-2", - "--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-user-sub2", - "--spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(User.class.getName()), - "--spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=AVRO")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: User{name='user21', age=21}")); - } - } - - @Test - void avroTypeUserWithoutSchemaTypeWithCustomMappingsViaCustomizer(CapturedOutput output) { - SpringApplication app = new SpringApplication(AvroUserConfigCustomMappings.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=userSupplier;userLogger", - "--spring.cloud.stream.bindings.userSupplier-out-0.destination=user-stream-3", - "--spring.cloud.stream.bindings.userLogger-in-0.destination=user-stream-3", - "--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-user-sub3")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: User{name='user21', age=21}")); - } - } - - @Test - void keyValueAvroTypeWithSchemaTypeAndCustomTypeMappingsViaProps(CapturedOutput output) { - SpringApplication app = new SpringApplication(KeyValueAvroUserConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=userSupplier;userLogger", - "--spring.cloud.stream.bindings.userSupplier-out-0.destination=kv-stream-1", - "--spring.cloud.stream.bindings.userLogger-in-0.destination=kv-stream-1", - "--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.schema-type=KEY_VALUE", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.schema-type=KEY_VALUE", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-kv-sub1", - "--spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(User.class.getName()), - "--spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=AVRO")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: 21->User{name='user21', age=21}")); - } - } - - @Test - void keyValueAvroTypeWithoutSchemaTypeAndCustomTypeMappingsViaProps(CapturedOutput output) { - SpringApplication app = new SpringApplication(KeyValueAvroUserConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=userSupplier;userLogger", - "--spring.cloud.stream.bindings.userSupplier-out-0.destination=kv-stream-2", - "--spring.cloud.stream.bindings.userLogger-in-0.destination=kv-stream-2", - "--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-kv-sub2", - "--spring.pulsar.defaults.type-mappings[0].message-type=%s".formatted(User.class.getName()), - "--spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=AVRO")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: 21->User{name='user21', age=21}")); - } - } - - @Test - void keyValueAvroTypeWithSchemaTypeAndCustomTypeMappingsViaCustomizer(CapturedOutput output) { - SpringApplication app = new SpringApplication(KeyValueAvroUserConfigCustomMappings.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=userSupplier;userLogger", - "--spring.cloud.stream.bindings.userSupplier-out-0.destination=kv-stream-3", - "--spring.cloud.stream.bindings.userLogger-in-0.destination=kv-stream-3", - "--spring.cloud.stream.bindings.userSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.schema-type=KEY_VALUE", - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userSupplier-out-0.producer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.bindings.userLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.schema-type=KEY_VALUE", - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-type=" - + User.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.pulsar.bindings.userLogger-in-0.consumer.subscription-name=pbit-kv-sub3")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: 21->User{name='user21', age=21}")); - } - } - - @Test - void keyValueJsonTypeWithoutSchemaTypeAndWithoutCustomTypeMappings(CapturedOutput output) { - SpringApplication app = new SpringApplication(KeyValueJsonFooConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=fooSupplier;fooLogger", - "--spring.cloud.stream.bindings.fooSupplier-out-0.destination=kv-stream-4", - "--spring.cloud.stream.bindings.fooLogger-in-0.destination=kv-stream-4", - "--spring.cloud.stream.bindings.fooSupplier-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-type=" - + Foo.class.getName(), - "--spring.cloud.stream.pulsar.bindings.fooSupplier-out-0.producer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.bindings.fooLogger-in-0.consumer.use-native-decoding=true", - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-type=" + Foo.class.getName(), - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.message-key-type=" - + String.class.getName(), - "--spring.cloud.stream.pulsar.bindings.fooLogger-in-0.consumer.subscription-name=pbit-kv-sub4")) { - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: 5150->Foo[value=5150]")); - } - } - - } - - @Nested - class CustomMessageHeaders { - - @Test - void headersPropagatedSendAndReceive(CapturedOutput output) { - SpringApplication app = new SpringApplication(CustomSimpleHeadersConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=springMessageSupplier;springMessageLogger", - "--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-1", - "--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-1", - "--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh1-sub1")) { - // Wait for a few of the messages to flow through (check for index = 5) - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)).until( - () -> output.toString().contains("Hello binder: test-headers-msg-5 w/ custom-id: 5150-5")); - } - } - - @Test - void complexHeadersAreEncodedAndPropagated(CapturedOutput output) { - SpringApplication app = new SpringApplication(CustomComplexHeadersConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=springMessageSupplier;springMessageLogger", - "--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-2", - "--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-2", - "--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh2-sub1")) { - // Wait for a few of the messages to flow through (check for index = 5) - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)).until(() -> output.toString() - .contains("Hello binder: test-headers-msg-5 w/ custom-id: FooHeader[value=5150-5]")); - } - } - - @Test - void producerHeaderModeNone(CapturedOutput output) { - SpringApplication app = new SpringApplication(CustomComplexHeadersConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=springMessageSupplier;springMessageLogger", - "--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-3", - "--spring.cloud.stream.bindings.springMessageSupplier-out-0.producer.header-mode=none", - "--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-3", - "--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh3-sub1")) { - // Wait for a few of the messages to flow through (check for index = 5) - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: test-headers-msg-5 w/ custom-id: null")); - } - } - - @Test - void consumerHeaderModeNone(CapturedOutput output) { - SpringApplication app = new SpringApplication(CustomComplexHeadersConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=springMessageSupplier;springMessageLogger", - "--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-4", - "--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-4", - "--spring.cloud.stream.bindings.springMessageLogger-in-0.consumer.header-mode=none", - "--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh4-sub1")) { - // Wait for a few of the messages to flow through (check for index = 5) - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)) - .until(() -> output.toString().contains("Hello binder: test-headers-msg-5 w/ custom-id: null")); - } - } - - @Test - void customHeaderMapperRespected(CapturedOutput output) { - SpringApplication app = new SpringApplication(CustomHeaderMapperConfig.class); - app.setWebApplicationType(WebApplicationType.NONE); - try (ConfigurableApplicationContext ignored = app.run( - "--spring.pulsar.client.service-url=" + PulsarTestContainerSupport.getPulsarBrokerUrl(), - "--spring.pulsar.administration.service-url=" + PulsarTestContainerSupport.getHttpServiceUrl(), - "--spring.cloud.function.definition=springMessageSupplier;springMessageLogger", - "--spring.cloud.stream.bindings.springMessageSupplier-out-0.destination=cmh-5", - "--spring.cloud.stream.bindings.springMessageLogger-in-0.destination=cmh-5", - "--spring.cloud.stream.pulsar.bindings.springMessageLogger-in-0.consumer.subscription-name=pbit-cmh5-sub1")) { - // Wait for a few of the messages to flow through (check for index = 5) - Awaitility.await().atMost(Duration.ofSeconds(AWAIT_DURATION)).until(() -> output.toString() - .contains("Hello binder: test-headers-msg-5 w/ custom-id: tsh->tph->FooHeader[value=5150-5]")); - } - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class CustomSimpleHeadersConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private int msgCount = 0; - - @Bean - public Supplier> springMessageSupplier() { - return () -> { - msgCount++; - return MessageBuilder.withPayload("test-headers-msg-" + msgCount) - .setHeader("custom-id", "5150-" + msgCount).build(); - }; - } - - @Bean - public Consumer> springMessageLogger() { - return s -> this.logger.info("Hello binder: {} w/ custom-id: {}", s.getPayload(), - s.getHeaders().get("custom-id")); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class CustomComplexHeadersConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private int msgCount = 0; - - @Bean - public Supplier> springMessageSupplier() { - return () -> { - msgCount++; - return MessageBuilder.withPayload("test-headers-msg-" + msgCount) - .setHeader("custom-id", new FooHeader("5150-" + msgCount)).build(); - }; - } - - @Bean - public Consumer> springMessageLogger() { - return s -> { - var header = s.getHeaders().get("custom-id"); - if (header != null) { - assertThat(header).isInstanceOf(FooHeader.class); - } - this.logger.info("Hello binder: {} w/ custom-id: {}", s.getPayload(), header); - }; - } - - record FooHeader(String value) { - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class CustomHeaderMapperConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private int msgCount = 0; - - @Bean - public PulsarHeaderMapper extendedToStringHeaderMapper() { - return new ToStringPulsarHeaderMapper(List.of("custom-id"), List.of("foo", "custom-id")) { - @Override - public Map toPulsarHeaders(MessageHeaders springHeaders) { - Map pulsarHeaders = super.toPulsarHeaders(springHeaders); - // foo and custom-id are allowed and expected - assertThat(pulsarHeaders).containsKeys("foo", "custom-id"); - return pulsarHeaders; - } - - @Override - public MessageHeaders toSpringHeaders(org.apache.pulsar.client.api.Message pulsarMessage) { - MessageHeaders springHeaders = super.toSpringHeaders(pulsarMessage); - // foo not allowed, custom-id allowed - assertThat(springHeaders).doesNotContainKey("foo").containsKey("custom-id"); - return springHeaders; - } - - @Override - protected String toPulsarHeaderValue(String name, Object value, Object context) { - return "tph->" + super.toPulsarHeaderValue(name, value, context); - } - - @Override - protected Object toSpringHeaderValue(String headerName, String rawHeader, Object context) { - return "tsh->" + super.toSpringHeaderValue(headerName, rawHeader, context); - } - }; - } - - @Bean - public Supplier> springMessageSupplier() { - return () -> { - msgCount++; - return MessageBuilder.withPayload("test-headers-msg-" + msgCount) - .setHeader("foo", "bar-" + msgCount) - .setHeader("custom-id", new FooHeader("5150-" + msgCount)).build(); - }; - } - - @Bean - public Consumer> springMessageLogger() { - return s -> { - var header = s.getHeaders().get("custom-id"); - if (header != null) { - assertThat(header).isInstanceOf(String.class); - } - var fooHeader = s.getHeaders().get("foo"); - assertThat(fooHeader).isNull(); - this.logger.info("Hello binder: {} w/ custom-id: {}", s.getPayload(), header); - }; - } - - record FooHeader(String value) { - } - - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class PrimitiveTextConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier textSupplier() { - return () -> "test-basic-scenario"; - } - - @Bean - public Consumer textLogger() { - return s -> this.logger.info("Hello binder: " + s); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(PrimitiveTextConfig.class) - static class BinderAndBindingPropsTestConfig { - - @SuppressWarnings("unchecked") - @Bean - public PulsarProducerFactory pulsarProducerFactory(PulsarClient pulsarClient, - PulsarProperties pulsarProperties, TopicResolver topicResolver) { - var customizer = (ProducerBuilderCustomizer) pulsarProperties.getProducer() - .toProducerBuilderCustomizer(); - return new TrackingProducerFactory(pulsarClient, pulsarProperties.getProducer().getTopicName(), customizer, - topicResolver); - } - - @SuppressWarnings("unchecked") - @Bean - public PulsarConsumerFactory pulsarConsumerFactory(PulsarClient pulsarClient, - PulsarProperties pulsarProperties) { - var customizer = (ConsumerBuilderCustomizer) pulsarProperties.getConsumer() - .toConsumerBuilderCustomizer(); - return new TrackingConsumerFactory(pulsarClient, customizer); - } - - } - - static class TrackingProducerFactory extends DefaultPulsarProducerFactory { - - List> producersCreated = new ArrayList<>(); - - TrackingProducerFactory(PulsarClient pulsarClient, @Nullable String defaultTopic, - ProducerBuilderCustomizer defaultConfigCustomizer, TopicResolver topicResolver) { - super(pulsarClient, defaultTopic, defaultConfigCustomizer, topicResolver); - } - - @Override - protected Producer doCreateProducer(Schema schema, @Nullable String topic, - @Nullable Collection encryptionKeys, - @Nullable List> producerBuilderCustomizers) - throws PulsarClientException { - Producer producer = super.doCreateProducer(schema, topic, encryptionKeys, - producerBuilderCustomizers); - producersCreated.add(producer); - return producer; - } - - } - - static class TrackingConsumerFactory extends DefaultPulsarConsumerFactory { - - List> consumersCreated = new ArrayList<>(); - - TrackingConsumerFactory(PulsarClient pulsarClient, ConsumerBuilderCustomizer defaultConsumerConfig) { - super(pulsarClient, defaultConsumerConfig); - } - - @Override - public org.apache.pulsar.client.api.Consumer createConsumer(Schema schema, - @Nullable Collection topics, @Nullable String subscriptionName, - @Nullable Map metadataProperties, - @Nullable List> consumerBuilderCustomizers) - throws PulsarClientException { - org.apache.pulsar.client.api.Consumer consumer = super.createConsumer(schema, topics, - subscriptionName, metadataProperties, consumerBuilderCustomizers); - consumersCreated.add(consumer); - return consumer; - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class PrimitiveFloatConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier piSupplier() { - return () -> 3.14f; - } - - @Bean - public Consumer piLogger() { - return f -> this.logger.info("Hello binder: " + f); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class JsonFooConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier fooSupplier() { - return () -> new Foo("5150"); - } - - @Bean - public Consumer fooLogger() { - return f -> this.logger.info("Hello binder: " + f); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(JsonFooConfig.class) - static class JsonFooWithCustomMappingConfig { - - @Bean - public SchemaResolverCustomizer customMappings() { - return (resolver) -> resolver.addCustomSchemaMapping(Foo.class, JSONSchema.of(Foo.class)); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class AvroUserConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier userSupplier() { - return () -> new User("user21", 21); - } - - @Bean - public Consumer userLogger() { - return f -> this.logger.info("Hello binder: " + f); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(AvroUserConfig.class) - static class AvroUserConfigCustomMappings { - - @Bean - public SchemaResolverCustomizer customMappings() { - return (resolver) -> resolver.addCustomSchemaMapping(User.class, Schema.AVRO(User.class)); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class KeyValueAvroUserConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier> userSupplier() { - return () -> new KeyValue<>("21", new User("user21", 21)); - } - - @Bean - public Consumer> userLogger() { - return f -> this.logger.info("Hello binder: " + f.getKey() + "->" + f.getValue()); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - @Import(KeyValueAvroUserConfig.class) - static class KeyValueAvroUserConfigCustomMappings { - - @Bean - public SchemaResolverCustomizer customMappings() { - return (resolver) -> resolver.addCustomSchemaMapping(User.class, Schema.AVRO(User.class)); - } - - } - - @EnableAutoConfiguration - @SpringBootConfiguration - static class KeyValueJsonFooConfig { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Bean - public Supplier> fooSupplier() { - return () -> new KeyValue<>("5150", new Foo("5150")); - } - - @Bean - public Consumer> fooLogger() { - return f -> this.logger.info("Hello binder: " + f.getKey() + "->" + f.getValue()); - } - - } - - record Foo(String value) { - } - - /** - * Do not convert this to a Record as Avro does not seem to work well w/ records. - */ - static class User { - - private String name; - - private int age; - - User() { - } - - User(String name, int age) { - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - User user = (User) o; - return age == user.age && Objects.equals(name, user.name); - } - - @Override - public int hashCode() { - return Objects.hash(name, age); - } - - @Override - public String toString() { - return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; - } - - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderTests.java deleted file mode 100644 index a412d3890..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderTests.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.pulsar.client.api.PulsarClient; -import org.apache.pulsar.client.api.PulsarClientException; -import org.apache.pulsar.client.api.SubscriptionInitialPosition; -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.junit.jupiter.api.TestInfo; - -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; -import org.springframework.cloud.stream.binder.Spy; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarProducerProperties; -import org.springframework.cloud.stream.binder.pulsar.provisioning.PulsarTopicProvisioner; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.pulsar.core.DefaultPulsarConsumerFactory; -import org.springframework.pulsar.core.DefaultPulsarProducerFactory; -import org.springframework.pulsar.core.DefaultSchemaResolver; -import org.springframework.pulsar.core.PulsarAdministration; -import org.springframework.pulsar.core.PulsarTemplate; -import org.springframework.pulsar.support.header.JsonPulsarHeaderMapper; -import org.springframework.util.Assert; -import org.springframework.util.MimeTypeUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PulsarMessageChannelBinder}. - * - * @author Soby Chacko - */ -public class PulsarBinderTests extends - PartitionCapableBinderTests, ExtendedProducerProperties> - implements PulsarTestContainerSupport { - - private PulsarTestBinder binder; - - @Nullable - protected PulsarClient pulsarClient; - - @BeforeEach - void createPulsarClient() throws PulsarClientException { - pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl()).build(); - } - - @AfterEach - void closePulsarClient() throws PulsarClientException { - if (pulsarClient != null && !pulsarClient.isClosed()) { - pulsarClient.close(); - } - } - - @Override - protected boolean usesExplicitRouting() { - return false; - } - - @Override - protected String getClassUnderTestName() { - return PulsarMessageChannelBinder.class.getSimpleName(); - } - - @Override - protected PulsarTestBinder getBinder() { - var pulsarAdministration = new PulsarAdministration(PulsarTestContainerSupport.getHttpServiceUrl()); - var configProps = new PulsarBinderConfigurationProperties(); - var provisioner = new PulsarTopicProvisioner(pulsarAdministration, configProps); - var producerFactory = new DefaultPulsarProducerFactory<>(pulsarClient); - var pulsarTemplate = new PulsarTemplate<>(producerFactory); - var consumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, - (consumerBuilder -> consumerBuilder.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest))); - if (this.binder == null) { - this.binder = new PulsarTestBinder(provisioner, pulsarTemplate, consumerFactory, configProps, - new DefaultSchemaResolver(), JsonPulsarHeaderMapper.builder().build()); - } - return this.binder; - } - - @Override - protected ExtendedConsumerProperties createConsumerProperties() { - final ExtendedConsumerProperties pulsarConsumerProperties = new ExtendedConsumerProperties<>( - new PulsarConsumerProperties()); - return pulsarConsumerProperties; - } - - @Override - public Spy spyOn(String name) { - return null; - } - - private ExtendedProducerProperties createProducerProperties() { - return this.createProducerProperties(null); - } - - @Override - protected ExtendedProducerProperties createProducerProperties(TestInfo testInto) { - return new ExtendedProducerProperties<>(new PulsarProducerProperties()); - } - - @Override - protected void binderBindUnbindLatency() throws InterruptedException { - Thread.sleep(500); - } - - @Test - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void testSendAndReceive(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - BindingProperties outputBindingProperties = createProducerBindingProperties(createProducerProperties()); - - DirectChannel moduleOutputChannel = createBindableChannel("output", outputBindingProperties); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - - Binding producerBinding = binder.bindProducer("foo.bar", moduleOutputChannel, - outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer("foo.bar", null, moduleInputChannel, - consumerProperties); - - Message message = MessageBuilder - .withPayload("foo".getBytes(StandardCharsets.UTF_8)).build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference<>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat(new String(inboundMessageRef.get().getPayload(), StandardCharsets.UTF_8)).isEqualTo("foo"); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void testAnonymousGroup(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties(testInfo)); - DirectChannel output = createBindableChannel("output", producerBindingProperties); - Binding producerBinding = binder.bindProducer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), output, - producerBindingProperties.getProducer()); - - QueueChannel input1 = new QueueChannel(); - Binding binding1 = binder.bindConsumer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input1, - createConsumerProperties()); - - QueueChannel input2 = new QueueChannel(); - Binding binding2 = binder.bindConsumer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input2, - createConsumerProperties()); - - String testPayload1 = "foo-" + UUID.randomUUID(); - output.send(MessageBuilder.withPayload(testPayload1) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build()); - - Message receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload1); - - Message receivedMessage2 = (Message) receive(input2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1); - - binding2.unbind(); - - String testPayload2 = "foo-" + UUID.randomUUID(); - output.send(MessageBuilder.withPayload(testPayload2) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build()); - - binding2 = binder.bindConsumer(String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, input2, - createConsumerProperties()); - String testPayload3 = "foo-" + UUID.randomUUID(); - output.send(MessageBuilder.withPayload(testPayload3) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build()); - - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload2); - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload3); - - receivedMessage2 = (Message) receive(input2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1); - - producerBinding.unbind(); - binding1.unbind(); - binding2.unbind(); - } - - @Test - @Override - @Disabled - public void testPartitionedModuleSpEL(TestInfo testInfo) { - // This use-case needs to be further evaluated for Pulsar binder. - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderUtilsTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderUtilsTests.java deleted file mode 100644 index 17805d225..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarBinderUtilsTests.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -import org.apache.pulsar.client.api.CompressionType; -import org.apache.pulsar.client.api.ConsumerCryptoFailureAction; -import org.apache.pulsar.client.api.DeadLetterPolicy; -import org.apache.pulsar.client.api.HashingScheme; -import org.apache.pulsar.client.api.MessageRoutingMode; -import org.apache.pulsar.client.api.ProducerAccessMode; -import org.apache.pulsar.client.api.ProducerCryptoFailureAction; -import org.apache.pulsar.client.api.RegexSubscriptionMode; -import org.apache.pulsar.client.api.SubscriptionInitialPosition; -import org.apache.pulsar.client.api.SubscriptionMode; -import org.apache.pulsar.client.api.SubscriptionType; -import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils; -import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData; -import org.apache.pulsar.client.impl.conf.ProducerConfigurationData; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.pulsar.autoconfigure.ConsumerConfigProperties; -import org.springframework.pulsar.autoconfigure.ProducerConfigProperties; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Unit tests for {@link PulsarBinderUtils}. - * - * @author Soby Chacko - * @author Chris Bono - */ -public class PulsarBinderUtilsTests { - - @Nested - class SubscriptionNameTests { - - @Test - void respectsValueWhenSetAsProperty() { - var consumerDestination = mock(ConsumerDestination.class); - var pulsarConsumerProperties = mock(PulsarConsumerProperties.class); - when(pulsarConsumerProperties.getSubscriptionName()).thenReturn("my-sub"); - assertThat(PulsarBinderUtils.subscriptionName(pulsarConsumerProperties, consumerDestination)) - .isEqualTo("my-sub"); - } - - @Test - void generatesValueWhenNotSetAsProperty() { - var consumerDestination = mock(ConsumerDestination.class); - var pulsarConsumerProperties = mock(PulsarConsumerProperties.class); - when(pulsarConsumerProperties.getSubscriptionName()).thenReturn(null); - when(consumerDestination.getName()).thenReturn("my-topic"); - assertThat(PulsarBinderUtils.subscriptionName(pulsarConsumerProperties, consumerDestination)) - .startsWith("my-topic-anon-subscription-"); - } - - } - - @Nested - class MergedPropertiesTests { - - @ParameterizedTest(name = "{0}") - @MethodSource("mergePropertiesTestProvider") - void mergePropertiesTest(String testName, Map baseProps, Map binderProps, - Map bindingProps, Map expectedMergedProps) { - assertThat(PulsarBinderUtils.mergePropertiesWithPrecedence(baseProps, binderProps, bindingProps)) - .containsExactlyInAnyOrderEntriesOf(expectedMergedProps); - } - - // @formatter:off - static Stream mergePropertiesTestProvider() { - return Stream.of( - arguments("binderLevelContainsSamePropAsBaseWithDiffValue", - Map.of("foo", "foo-base"), - Map.of("foo", "foo-binder"), - Collections.emptyMap(), - Map.of("foo", "foo-binder")), - arguments("binderLevelContainsNewPropNotInBase", - Collections.emptyMap(), - Map.of("foo", "foo-binder"), - Collections.emptyMap(), - Map.of("foo", "foo-binder")), - arguments("binderLevelContainsSamePropAsBaseWithSameValue", - Map.of("foo", "foo-base"), - Map.of("foo", "foo-base"), - Collections.emptyMap(), - Collections.emptyMap()), - arguments("bindingLevelContainsSamePropAsBaseWithDiffValue", - Map.of("foo", "foo-base"), - Collections.emptyMap(), - Map.of("foo", "foo-binding"), - Map.of("foo", "foo-binding")), - arguments("bindingLevelContainsNewPropNotInBase", - Collections.emptyMap(), - Map.of("foo", "foo-binding"), - Collections.emptyMap(), - Map.of("foo", "foo-binding")), - arguments("bindingLevelContainsSamePropAsBaseWithSameValue", - Map.of("foo", "foo-base"), - Collections.emptyMap(), - Map.of("foo", "foo-base"), - Collections.emptyMap()), - arguments("bindingOverridesBinder", - Map.of("bar", "bar-base"), - Map.of("foo", "foo-binder"), - Map.of("foo", "foo-binding"), - Map.of("foo", "foo-binding")), - arguments("binderOverridesBaseAndBindingOverridesBinder", - Map.of("foo", "foo-base"), - Map.of("foo", "foo-binder"), - Map.of("foo", "foo-binding"), - Map.of("foo", "foo-binding")), - arguments("onlyBaseProps", - Map.of("foo", "foo-base"), - Collections.emptyMap(), - Collections.emptyMap(), - Collections.emptyMap())); - } - // @formatter:on - - } - - @Nested - class ConvertedPropertiesTests { - - private final ProducerConfigProperties properties = new ProducerConfigProperties(); - - private void bind(Map map) { - ConfigurationPropertySource source = new MapConfigurationPropertySource(map); - new Binder(source).bind("spring.pulsar.producer", Bindable.ofInstance(this.properties)); - } - - @Test - void producerPropertiesToMap() { - Map props = new HashMap<>(); - props.put("spring.pulsar.producer.topic-name", "my-topic"); - props.put("spring.pulsar.producer.producer-name", "my-producer"); - props.put("spring.pulsar.producer.send-timeout", "2s"); - props.put("spring.pulsar.producer.block-if-queue-full", "true"); - props.put("spring.pulsar.producer.max-pending-messages", "3"); - props.put("spring.pulsar.producer.max-pending-messages-across-partitions", "4"); - props.put("spring.pulsar.producer.message-routing-mode", "custompartition"); - props.put("spring.pulsar.producer.hashing-scheme", "murmur3_32hash"); - props.put("spring.pulsar.producer.crypto-failure-action", "send"); - props.put("spring.pulsar.producer.batching-max-publish-delay", "5s"); - props.put("spring.pulsar.producer.batching-partition-switch-frequency-by-publish-delay", "6"); - props.put("spring.pulsar.producer.batching-max-messages", "7"); - props.put("spring.pulsar.producer.batching-max-bytes", "8"); - props.put("spring.pulsar.producer.batching-enabled", "false"); - props.put("spring.pulsar.producer.chunking-enabled", "true"); - props.put("spring.pulsar.producer.encryption-keys[0]", "my-key"); - props.put("spring.pulsar.producer.compression-type", "lz4"); - props.put("spring.pulsar.producer.initial-sequence-id", "9"); - props.put("spring.pulsar.producer.producer-access-mode", "exclusive"); - props.put("spring.pulsar.producer.lazy-start=partitioned-producers", "true"); - props.put("spring.pulsar.producer.properties[my-prop]", "my-prop-value"); - - bind(props); - Map producerProps = PulsarBinderUtils.convertProducerPropertiesToMap(properties); - - // Verify that the props can be loaded in a ProducerBuilder - assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(producerProps, - new ProducerConfigurationData(), ProducerConfigurationData.class)); - - assertThat(producerProps).containsEntry("topicName", "my-topic") - .containsEntry("producerName", "my-producer").containsEntry("sendTimeoutMs", 2_000) - .containsEntry("blockIfQueueFull", true).containsEntry("maxPendingMessages", 3) - .containsEntry("maxPendingMessagesAcrossPartitions", 4) - .containsEntry("messageRoutingMode", MessageRoutingMode.CustomPartition) - .containsEntry("hashingScheme", HashingScheme.Murmur3_32Hash) - .containsEntry("cryptoFailureAction", ProducerCryptoFailureAction.SEND) - .containsEntry("batchingMaxPublishDelayMicros", 5_000_000L) - .containsEntry("batchingPartitionSwitchFrequencyByPublishDelay", 6) - .containsEntry("batchingMaxMessages", 7).containsEntry("batchingMaxBytes", 8) - .containsEntry("batchingEnabled", false).containsEntry("chunkingEnabled", true) - .hasEntrySatisfying("encryptionKeys", - keys -> assertThat(keys).asInstanceOf(InstanceOfAssertFactories.collection(String.class)) - .containsExactly("my-key")) - .containsEntry("compressionType", CompressionType.LZ4).containsEntry("initialSequenceId", 9L) - .containsEntry("accessMode", ProducerAccessMode.Exclusive) - .containsEntry("lazyStartPartitionedProducers", true).hasEntrySatisfying("properties", - properties -> assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-prop", "my-prop-value")); - } - - } - - @Nested - class ConvertedConsumerPropertiesTests { - - private final ConsumerConfigProperties properties = new ConsumerConfigProperties(); - - private void bind(Map map) { - ConfigurationPropertySource source = new MapConfigurationPropertySource(map); - new Binder(source).bind("spring.pulsar.consumer", Bindable.ofInstance(this.properties)); - } - - @Test - void consumerPropertiesToMap() { - Map props = new HashMap<>(); - props.put("spring.pulsar.consumer.topics[0]", "my-topic"); - props.put("spring.pulsar.consumer.topics-pattern", "my-pattern"); - props.put("spring.pulsar.consumer.subscription-name", "my-subscription"); - props.put("spring.pulsar.consumer.subscription-type", "shared"); - props.put("spring.pulsar.consumer.subscription-properties[my-sub-prop]", "my-sub-prop-value"); - props.put("spring.pulsar.consumer.subscription-mode", "nondurable"); - props.put("spring.pulsar.consumer.receiver-queue-size", "1"); - props.put("spring.pulsar.consumer.acknowledgements-group-time", "2s"); - props.put("spring.pulsar.consumer.negative-ack-redelivery-delay", "3s"); - props.put("spring.pulsar.consumer.max-total-receiver-queue-size-across-partitions", "5"); - props.put("spring.pulsar.consumer.consumer-name", "my-consumer"); - props.put("spring.pulsar.consumer.ack-timeout", "6s"); - props.put("spring.pulsar.consumer.tick-duration", "7s"); - props.put("spring.pulsar.consumer.priority-level", "8"); - props.put("spring.pulsar.consumer.crypto-failure-action", "discard"); - props.put("spring.pulsar.consumer.properties[my-prop]", "my-prop-value"); - props.put("spring.pulsar.consumer.read-compacted", "true"); - props.put("spring.pulsar.consumer.subscription-initial-position", "earliest"); - props.put("spring.pulsar.consumer.pattern-auto-discovery-period", "9"); - props.put("spring.pulsar.consumer.regex-subscription-mode", "all-topics"); - props.put("spring.pulsar.consumer.dead-letter-policy.max-redeliver-count", "4"); - props.put("spring.pulsar.consumer.dead-letter-policy.retry-letter-topic", "my-retry-topic"); - props.put("spring.pulsar.consumer.dead-letter-policy.dead-letter-topic", "my-dlt-topic"); - props.put("spring.pulsar.consumer.dead-letter-policy.initial-subscription-name", "my-initial-subscription"); - props.put("spring.pulsar.consumer.retry-enable", "true"); - props.put("spring.pulsar.consumer.auto-update-partitions", "false"); - props.put("spring.pulsar.consumer.auto-update-partitions-interval", "10s"); - props.put("spring.pulsar.consumer.replicate-subscription-state", "true"); - props.put("spring.pulsar.consumer.reset-include-head", "true"); - props.put("spring.pulsar.consumer.batch-index-ack-enabled", "true"); - props.put("spring.pulsar.consumer.ack-receipt-enabled", "true"); - props.put("spring.pulsar.consumer.pool-messages", "true"); - props.put("spring.pulsar.consumer.start-paused", "true"); - props.put("spring.pulsar.consumer.auto-ack-oldest-chunked-message-on-queue-full", "false"); - props.put("spring.pulsar.consumer.max-pending-chunked-message", "11"); - props.put("spring.pulsar.consumer.expire-time-of-incomplete-chunked-message", "12s"); - - bind(props); - Map consumerProps = PulsarBinderUtils.convertConsumerPropertiesToMap(properties); - - // Verify that the props can be loaded in a ConsumerBuilder - assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(consumerProps, - new ConsumerConfigurationData<>(), ConsumerConfigurationData.class)); - - assertThat(consumerProps) - .hasEntrySatisfying("topicNames", - topics -> assertThat(topics) - .asInstanceOf(InstanceOfAssertFactories.collection(String.class)) - .containsExactly("my-topic")) - .hasEntrySatisfying("topicsPattern", p -> assertThat(p.toString()).isEqualTo("my-pattern")) - .containsEntry("subscriptionName", "my-subscription") - .containsEntry("subscriptionType", SubscriptionType.Shared) - .hasEntrySatisfying("subscriptionProperties", - properties -> assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-sub-prop", "my-sub-prop-value")) - .containsEntry("subscriptionMode", SubscriptionMode.NonDurable) - .containsEntry("receiverQueueSize", 1).containsEntry("acknowledgementsGroupTimeMicros", 2_000_000L) - .containsEntry("negativeAckRedeliveryDelayMicros", 3_000_000L) - .containsEntry("maxTotalReceiverQueueSizeAcrossPartitions", 5) - .containsEntry("consumerName", "my-consumer").containsEntry("ackTimeoutMillis", 6_000L) - .containsEntry("tickDurationMillis", 7_000L).containsEntry("priorityLevel", 8) - .containsEntry("cryptoFailureAction", ConsumerCryptoFailureAction.DISCARD) - .hasEntrySatisfying("properties", - properties -> assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-prop", "my-prop-value")) - .containsEntry("readCompacted", true) - .containsEntry("subscriptionInitialPosition", SubscriptionInitialPosition.Earliest) - .containsEntry("patternAutoDiscoveryPeriod", 9) - .containsEntry("regexSubscriptionMode", RegexSubscriptionMode.AllTopics) - .hasEntrySatisfying("deadLetterPolicy", dlp -> { - DeadLetterPolicy deadLetterPolicy = (DeadLetterPolicy) dlp; - assertThat(deadLetterPolicy.getMaxRedeliverCount()).isEqualTo(4); - assertThat(deadLetterPolicy.getRetryLetterTopic()).isEqualTo("my-retry-topic"); - assertThat(deadLetterPolicy.getDeadLetterTopic()).isEqualTo("my-dlt-topic"); - assertThat(deadLetterPolicy.getInitialSubscriptionName()).isEqualTo("my-initial-subscription"); - }).containsEntry("retryEnable", true).containsEntry("autoUpdatePartitions", false) - .containsEntry("autoUpdatePartitionsIntervalSeconds", 10L) - .containsEntry("replicateSubscriptionState", true).containsEntry("resetIncludeHead", true) - .containsEntry("batchIndexAckEnabled", true).containsEntry("ackReceiptEnabled", true) - .containsEntry("poolMessages", true).containsEntry("startPaused", true) - .containsEntry("autoAckOldestChunkedMessageOnQueueFull", false) - .containsEntry("maxPendingChunkedMessage", 11) - .containsEntry("expireTimeOfIncompleteChunkedMessageMillis", 12_000L); - } - - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarExtendedBindingPropertiesTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarExtendedBindingPropertiesTests.java deleted file mode 100644 index c272e064b..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarExtendedBindingPropertiesTests.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.pulsar.client.api.ProducerAccessMode; -import org.apache.pulsar.client.api.SubscriptionMode; -import org.apache.pulsar.client.api.SubscriptionType; -import org.apache.pulsar.client.impl.conf.ConfigurationDataUtils; -import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData; -import org.apache.pulsar.client.impl.conf.ProducerConfigurationData; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarExtendedBindingProperties; -import org.springframework.pulsar.listener.PulsarContainerProperties; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; - -/** - * Tests for {@link PulsarExtendedBindingProperties}. - * - * @author Chris Bono - */ -public class PulsarExtendedBindingPropertiesTests { - - private final PulsarExtendedBindingProperties properties = new PulsarExtendedBindingProperties(); - - private void bind(Map map) { - ConfigurationPropertySource source = new MapConfigurationPropertySource(map); - new Binder(source).bind("spring.cloud.stream.pulsar", Bindable.ofInstance(this.properties)); - } - - @Test - void producerProperties() { - // Only spot check a few values (PulsarPropertiesTests does the heavy lifting) - Map props = new HashMap<>(); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.topic-name", "my-topic"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.send-timeout", "2s"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.max-pending-messages", "3"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.producer-access-mode", "exclusive"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.producer.properties[my-prop]", "my-prop-value"); - - bind(props); - - assertThat(properties.getBindings()).containsOnlyKeys("my-foo"); - Map producerProps = PulsarBinderUtils - .convertProducerPropertiesToMap(properties.getExtendedProducerProperties("my-foo")); - // Verify that the props can be loaded in a ProducerBuilder - assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(producerProps, - new ProducerConfigurationData(), ProducerConfigurationData.class)); - // @formatter:off - assertThat(producerProps) - .containsEntry("topicName", "my-topic") - .containsEntry("sendTimeoutMs", 2_000) - .containsEntry("maxPendingMessages", 3) - .containsEntry("accessMode", ProducerAccessMode.Exclusive) - .hasEntrySatisfying("properties", properties -> - assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-prop", "my-prop-value")); - // @formatter:on - } - - @Test - void consumerProperties() { - // Only spot check a few values (PulsarPropertiesTests does the heavy lifting) - Map props = new HashMap<>(); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.topics[0]", "my-topic"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-properties[my-sub-prop]", - "my-sub-prop-value"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-mode", "nondurable"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.receiver-queue-size", "1"); - - bind(props); - - assertThat(properties.getBindings()).containsOnlyKeys("my-foo"); - Map consumerProps = PulsarBinderUtils - .convertConsumerPropertiesToMap(properties.getExtendedConsumerProperties("my-foo")); - // Verify that the props can be loaded in a ConsumerBuilder - assertThatNoException().isThrownBy(() -> ConfigurationDataUtils.loadData(consumerProps, - new ConsumerConfigurationData<>(), ConsumerConfigurationData.class)); - // @formatter:off - assertThat(consumerProps) - .hasEntrySatisfying("topicNames", - topics -> assertThat(topics).asInstanceOf(InstanceOfAssertFactories.collection(String.class)) - .containsExactly("my-topic")) - .hasEntrySatisfying("subscriptionProperties", - properties -> assertThat(properties) - .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) - .containsEntry("my-sub-prop", "my-sub-prop-value")) - .containsEntry("subscriptionMode", SubscriptionMode.NonDurable) - .containsEntry("receiverQueueSize", 1); - // @formatter:on - } - - @Test - void extendedBindingsArePropagatedToContainerProperties() { - Map props = new HashMap<>(); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-name", "my-foo-sbscription"); - props.put("spring.cloud.stream.pulsar.bindings.my-foo.consumer.subscription-type", "Shared"); - - bind(props); - - var bindingConsumerProps = PulsarBinderUtils - .convertConsumerPropertiesToMap(properties.getExtendedConsumerProperties("my-foo")); - PulsarContainerProperties pulsarContainerProperties = new PulsarContainerProperties(); - pulsarContainerProperties.getPulsarConsumerProperties().putAll(bindingConsumerProps); - - assertThat(pulsarContainerProperties.getSubscriptionName()).isNull(); - assertThat(pulsarContainerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Exclusive); - - pulsarContainerProperties.updateContainerProperties(); - - assertThat(pulsarContainerProperties.getSubscriptionName()).isEqualTo("my-foo-sbscription"); - assertThat(pulsarContainerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared); - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTestBinder.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTestBinder.java deleted file mode 100644 index 6040c54cc..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTestBinder.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.pulsar.provisioning.PulsarTopicProvisioner; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.integration.config.EnableIntegration; -import org.springframework.pulsar.core.PulsarConsumerFactory; -import org.springframework.pulsar.core.PulsarTemplate; -import org.springframework.pulsar.core.SchemaResolver; -import org.springframework.pulsar.support.header.PulsarHeaderMapper; - -/** - * Test binder to exercise producer/consumer bindings in - * {@link PulsarMessageChannelBinder}. - * - * @author Soby Chacko - * @author Chris Bono - */ -public class PulsarTestBinder extends AbstractPulsarTestBinder { - - @SuppressWarnings({ "unchecked" }) - PulsarTestBinder(PulsarTopicProvisioner pulsarTopicProvisioner, PulsarTemplate pulsarTemplate, - PulsarConsumerFactory pulsarConsumerFactory, PulsarBinderConfigurationProperties binderConfigProps, - SchemaResolver schemaResolver, PulsarHeaderMapper headerMapper) { - - try { - var binder = new PulsarMessageChannelBinder(pulsarTopicProvisioner, (PulsarTemplate) pulsarTemplate, - pulsarConsumerFactory, binderConfigProps, schemaResolver, headerMapper); - var context = new AnnotationConfigApplicationContext(Config.class); - setApplicationContext(context); - binder.setApplicationContext(context); - binder.afterPropertiesSet(); - this.setPollableConsumerBinder(binder); - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Configuration - @EnableIntegration - static class Config { - - } - -} diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTestContainerSupport.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTestContainerSupport.java deleted file mode 100644 index b8bddb838..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTestContainerSupport.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import java.util.Locale; - -import org.junit.jupiter.api.BeforeAll; -import org.testcontainers.containers.PulsarContainer; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; - -/** - * Provides a static {@link PulsarContainer} that can be shared across test classes. - * - * @author Chris Bono - */ -@Testcontainers(disabledWithoutDocker = true) -public interface PulsarTestContainerSupport { - - PulsarContainer PULSAR_CONTAINER = new PulsarContainer(getPulsarImage()); - - @BeforeAll - static void startContainer() { - PULSAR_CONTAINER.start(); - } - - static String getPulsarBrokerUrl() { - return PULSAR_CONTAINER.getPulsarBrokerUrl(); - } - - static DockerImageName getPulsarImage() { - return isRunningOnMacM1() ? getMacM1PulsarImage() : getStandardPulsarImage(); - } - - static String getHttpServiceUrl() { - return PULSAR_CONTAINER.getHttpServiceUrl(); - } - - private static boolean isRunningOnMacM1() { - String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); - String osArchitecture = System.getProperty("os.arch").toLowerCase(Locale.ENGLISH); - return osName.contains("mac") && osArchitecture.equals("aarch64"); - } - - private static DockerImageName getStandardPulsarImage() { - return DockerImageName.parse("apachepulsar/pulsar:2.11.0"); - } - - private static DockerImageName getMacM1PulsarImage() { - return DockerImageName.parse("kezhenxu94/pulsar").asCompatibleSubstituteFor("apachepulsar/pulsar"); - } - -} - diff --git a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTopicProvisionerTests.java b/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTopicProvisionerTests.java deleted file mode 100644 index fb60f0e71..000000000 --- a/binders/pulsar-binder/spring-cloud-stream-binder-pulsar/src/test/java/org/springframework/cloud/stream/binder/pulsar/PulsarTopicProvisionerTests.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.pulsar; - -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarConsumerProperties; -import org.springframework.cloud.stream.binder.pulsar.properties.PulsarProducerProperties; -import org.springframework.cloud.stream.binder.pulsar.provisioning.PulsarTopicProvisioner; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.pulsar.core.PulsarAdministration; -import org.springframework.pulsar.core.PulsarTopic; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * @author Soby Chacko - */ -public class PulsarTopicProvisionerTests { - - @Test - void provisionThroughProducerBindingWithDefaultPartitioning() { - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties(); - PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); - PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration, - pulsarBinderConfigurationProperties); - ExtendedProducerProperties properties = new ExtendedProducerProperties<>( - new PulsarProducerProperties()); - ProducerDestination producerDestination = pulsarTopicProvisioner.provisionProducerDestination("foo", - properties); - verifyAndAssert(pulsarAdministration, producerDestination.getName(), "foo", 0); - } - - private static void verifyAndAssert(PulsarAdministration pulsarAdministration, String actualProducerDestination, - String expectedProducerDestination, int expectedPartitionCount) { - ArgumentCaptor pulsarTopicArgumentCaptor = ArgumentCaptor.forClass(PulsarTopic.class); - verify(pulsarAdministration, times(1)).createOrModifyTopics(pulsarTopicArgumentCaptor.capture()); - assertThat(actualProducerDestination).isEqualTo(expectedProducerDestination); - PulsarTopic pulsarTopic = pulsarTopicArgumentCaptor.getValue(); - assertThat(pulsarTopic.topicName()).isEqualTo(expectedProducerDestination); - assertThat(pulsarTopic.numberOfPartitions()).isEqualTo(expectedPartitionCount); - } - - @Test - void provisionThroughConsumerBindingWithDefaultPartitioning() { - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties(); - PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); - PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration, - pulsarBinderConfigurationProperties); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - new PulsarConsumerProperties()); - ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "", - properties); - verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 0); - } - - @Test - void provisioningOnProducerBindingWithPartitionsSetAtTheBinderProperties() { - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties(); - pulsarBinderConfigurationProperties.setPartitionCount(4); - PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); - PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration, - pulsarBinderConfigurationProperties); - ExtendedProducerProperties properties = new ExtendedProducerProperties<>( - new PulsarProducerProperties()); - ProducerDestination producerDestination = pulsarTopicProvisioner.provisionProducerDestination("foo", - properties); - verifyAndAssert(pulsarAdministration, producerDestination.getName(), "foo", 4); - } - - @Test - void provisioningOnProducerBindingWithPartitionsSetAtTheBindingProperties() { - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties(); - PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); - PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration, - pulsarBinderConfigurationProperties); - ExtendedProducerProperties properties = new ExtendedProducerProperties<>( - new PulsarProducerProperties()); - properties.getExtension().setPartitionCount(4); - ProducerDestination producerDestination = pulsarTopicProvisioner.provisionProducerDestination("foo", - properties); - verifyAndAssert(pulsarAdministration, producerDestination.getName(), "foo", 4); - } - - @Test - void provisionThroughConsumerBindingWithPartitionsSetAtTheBinderProperties() { - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties(); - pulsarBinderConfigurationProperties.setPartitionCount(4); - PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); - PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration, - pulsarBinderConfigurationProperties); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - new PulsarConsumerProperties()); - ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "", - properties); - verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 4); - } - - @Test - void provisionThroughConsumerBindingWithPartitionsSetAtTheBindingProperties() { - PulsarBinderConfigurationProperties pulsarBinderConfigurationProperties = new PulsarBinderConfigurationProperties(); - PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); - PulsarTopicProvisioner pulsarTopicProvisioner = new PulsarTopicProvisioner(pulsarAdministration, - pulsarBinderConfigurationProperties); - PulsarConsumerProperties pulsarConsumerProperties = new PulsarConsumerProperties(); - pulsarConsumerProperties.setPartitionCount(4); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - pulsarConsumerProperties); - ConsumerDestination consumerDestination = pulsarTopicProvisioner.provisionConsumerDestination("bar", "", - properties); - verifyAndAssert(pulsarAdministration, consumerDestination.getName(), "bar", 4); - } - -} diff --git a/binders/rabbit-binder/.gitignore b/binders/rabbit-binder/.gitignore deleted file mode 100644 index 0387e4d42..000000000 --- a/binders/rabbit-binder/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -/application.yml -/application.properties -asciidoctor.css -*~ -.#* -*# -target/ -build/ -bin/ -_site/ -.classpath -.project -.settings -.springBeans -.DS_Store -*.sw* -*.iml -*.ipr -*.iws -.idea/* -.factorypath -dump.rdb -.apt_generated -artifacts -.sts4-cache diff --git a/binders/rabbit-binder/.mvn/jvm.config b/binders/rabbit-binder/.mvn/jvm.config deleted file mode 100644 index 0e7dabeff..000000000 --- a/binders/rabbit-binder/.mvn/jvm.config +++ /dev/null @@ -1 +0,0 @@ --Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom \ No newline at end of file diff --git a/binders/rabbit-binder/.mvn/maven.config b/binders/rabbit-binder/.mvn/maven.config deleted file mode 100644 index 3b8cf46e1..000000000 --- a/binders/rabbit-binder/.mvn/maven.config +++ /dev/null @@ -1 +0,0 @@ --DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring diff --git a/binders/rabbit-binder/.mvn/wrapper/MavenWrapperDownloader.java b/binders/rabbit-binder/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index b901097f2..000000000 --- a/binders/rabbit-binder/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/binders/rabbit-binder/.mvn/wrapper/maven-wrapper.jar b/binders/rabbit-binder/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a55..000000000 Binary files a/binders/rabbit-binder/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/binders/rabbit-binder/.mvn/wrapper/maven-wrapper.properties b/binders/rabbit-binder/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572ce..000000000 --- a/binders/rabbit-binder/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/binders/rabbit-binder/.settings.xml b/binders/rabbit-binder/.settings.xml deleted file mode 100644 index 3da1ea205..000000000 --- a/binders/rabbit-binder/.settings.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - repo.spring.io - ${env.CI_DEPLOY_USERNAME} - ${env.CI_DEPLOY_PASSWORD} - - - - - - spring - true - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - - - diff --git a/binders/rabbit-binder/LICENSE b/binders/rabbit-binder/LICENSE deleted file mode 100644 index 9b259bdfc..000000000 --- a/binders/rabbit-binder/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ-stop.sh b/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ-stop.sh deleted file mode 100755 index a0f325b26..000000000 --- a/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ-stop.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -LOCAL_HOST="${LOCAL_HOST:-localhost}" -RETRIES="${RETRIES:-70}" - -PORT_TO_CHECK=5672 - -WAIT_TIME="${WAIT_TIME:-5}" - -function netcat_port() { - local PASSED_HOST="${2:-$LOCAL_HOST}" - local RABBITMQ_STOPPED=1 - local counter=1 - for i in $( seq 1 "${RETRIES}" ); do - ((counter++)) - if [ "${counter}" -gt 2 ] - then - echo "Rabbitmq is still running. Will try to stop again in [${WAIT_TIME}] seconds" - fi - sleep "${WAIT_TIME}" - nc -v -w 1 ${PASSED_HOST} $1 && continue - echo "Rabbitmq stopped..." - RABBITMQ_STOPPED=0 - break - done - return ${RABBITMQ_STOPPED} -} - -export -f netcat_port - -dockerComposeFile="docker-compose-RABBITMQ.yml" -docker-compose -f $dockerComposeFile kill - -RABBITMQ_STOPPED="no" - -echo "Waiting for RabbitMQ to stop for [$(( WAIT_TIME * RETRIES ))] seconds" -netcat_port $PORT_TO_CHECK && RABBITMQ_STOPPED="yes" - -if [[ "${RABBITMQ_STOPPED}" == "no" ]] ; then - echo "RabbitMQ failed to stop..." - exit 1 -fi \ No newline at end of file diff --git a/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ.sh b/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ.sh deleted file mode 100755 index 9e6a58d0e..000000000 --- a/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -LOCAL_HOST="${LOCAL_HOST:-localhost}" -SHOULD_START_RABBIT="${SHOULD_START_RABBIT:-yes}" -PORT_TO_CHECK=5672 - -WAIT_TIME="${WAIT_TIME:-5}" -RETRIES="${RETRIES:-70}" - -function netcat_port() { - local PASSED_HOST="${2:-$LOCAL_HOST}" - local READY_FOR_TESTS=1 - for i in $( seq 1 "${RETRIES}" ); do - sleep "${WAIT_TIME}" - nc -v -w 1 ${PASSED_HOST} $1 && READY_FOR_TESTS=0 && break - echo "Fail #$i/${RETRIES}... will try again in [${WAIT_TIME}] seconds" - done - return ${READY_FOR_TESTS} -} - -export -f netcat_port - -dockerComposeFile="docker-compose-RABBITMQ.yml" -docker-compose -f $dockerComposeFile kill -docker-compose -f $dockerComposeFile build - -if [[ "${SHOULD_START_RABBIT}" == "yes" ]] ; then - echo -e "\n\nBooting up RabbitMQ" - docker-compose -f $dockerComposeFile up -d rabbitmq -fi - -READY_FOR_TESTS="no" - -echo "Waiting for RabbitMQ to boot for [$(( WAIT_TIME * RETRIES ))] seconds" -netcat_port $PORT_TO_CHECK && READY_FOR_TESTS="yes" - -if [[ "${READY_FOR_TESTS}" == "no" ]] ; then - echo "RabbitMQ failed to start..." - exit 1 -fi - diff --git a/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ.yml b/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ.yml deleted file mode 100644 index deb4cf8d1..000000000 --- a/binders/rabbit-binder/ci-docker-compose/docker-compose-RABBITMQ.yml +++ /dev/null @@ -1,5 +0,0 @@ -rabbitmq: - image: rabbitmq:management - ports: - - 5672:5672 - - 15672:15672 \ No newline at end of file diff --git a/binders/rabbit-binder/mvnw b/binders/rabbit-binder/mvnw deleted file mode 100755 index 41c0f0c23..000000000 --- a/binders/rabbit-binder/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - 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" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/binders/rabbit-binder/mvnw.cmd b/binders/rabbit-binder/mvnw.cmd deleted file mode 100644 index 86115719e..000000000 --- a/binders/rabbit-binder/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/binders/rabbit-binder/pom.xml b/binders/rabbit-binder/pom.xml deleted file mode 100644 index 4758d4c46..000000000 --- a/binders/rabbit-binder/pom.xml +++ /dev/null @@ -1,181 +0,0 @@ - - - 4.0.0 - spring-cloud-stream-binder-rabbit-parent - 4.1.0-SNAPSHOT - pom - - org.springframework.cloud - spring-cloud-stream-binders - 4.1.0-SNAPSHOT - - - 17 - 4.1.0-SNAPSHOT - true - true - - true - - - - spring-cloud-stream-binder-rabbit-core - spring-cloud-stream-binder-rabbit - spring-cloud-starter-stream-rabbit - spring-cloud-stream-binder-rabbit-test-support - - - - org.junit.vintage - junit-vintage-engine - test - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${java.version} - ${java.version} - -parameters - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - spring - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/libs-release-local - - false - - - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - - spring-releases - Spring Releases - https://repo.spring.io/release - - - rsocket-snapshots - RSocket Snapshots - https://oss.jfrog.org/oss-snapshot-local - - true - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - - spring-releases - Spring Releases - https://repo.spring.io/libs-release-local - - - diff --git a/binders/rabbit-binder/spring-cloud-starter-stream-rabbit/pom.xml b/binders/rabbit-binder/spring-cloud-starter-stream-rabbit/pom.xml deleted file mode 100644 index 6a7b22c6f..000000000 --- a/binders/rabbit-binder/spring-cloud-starter-stream-rabbit/pom.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - 4.0.0 - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-parent - 4.1.0-SNAPSHOT - - spring-cloud-starter-stream-rabbit - Spring Cloud Starter Stream Rabbit - https://projects.spring.io/spring-cloud - - Pivotal Software, Inc. - https://www.spring.io - - - ${basedir}/../.. - - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit - - - diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/.jdk8 b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/pom.xml b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/pom.xml deleted file mode 100644 index b1857cec8..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-binder-rabbit-core - jar - spring-cloud-stream-binder-rabbit-core - RabbitMQ binder core - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-parent - 4.1.0-SNAPSHOT - - - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.boot - spring-boot-starter-amqp - - - org.springframework.amqp - spring-rabbit-stream - true - - - org.springframework.integration - spring-integration-amqp - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework - spring-web - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-test-support - test - - - org.springframework.boot - spring-boot-starter-webflux - - - io.projectreactor.netty - reactor-netty - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitAdminException.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitAdminException.java deleted file mode 100644 index fbfb32c19..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitAdminException.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder.rabbit.admin; - -/** - * Exceptions thrown while interfacing with the RabbitMQ admin plugin. - * - * @author Gary Russell - * @since 1.2 - */ -@SuppressWarnings("serial") -public class RabbitAdminException extends RuntimeException { - - public RabbitAdminException(String message, Throwable cause) { - super(message, cause); - } - - public RabbitAdminException(String message) { - super(message); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitBindingCleaner.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitBindingCleaner.java deleted file mode 100644 index d1e0dcf78..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/admin/RabbitBindingCleaner.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder.rabbit.admin; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.stream.binder.AbstractBinder; -import org.springframework.cloud.stream.binder.BindingCleaner; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.util.UriUtils; - -/** - * Implementation of {@link org.springframework.cloud.stream.binder.BindingCleaner} for - * the {@code RabbitBinder}. - * - * @author Gary Russell - * @author David Turanski - * @since 1.2 - */ -public class RabbitBindingCleaner implements BindingCleaner { - - private static final Log logger = LogFactory.getLog(RabbitBindingCleaner.class); - - private static final String PREFIX_DELIMITER = "."; - - /** - * Binder prefix. - */ - public static final String BINDER_PREFIX = "binder" + PREFIX_DELIMITER; - - @Override - public Map> clean(String entity, boolean isJob) { - return clean("http://localhost:15672/api", "guest", "guest", "/", BINDER_PREFIX, - entity, isJob); - } - - public Map> clean(String adminUri, String user, String pw, - String vhost, String binderPrefix, String entity, boolean isJob) { - - try { - WebClient client = WebClient.builder() - .filter(ExchangeFilterFunctions.basicAuthentication(user, pw)) - .build(); - URI uri = new URI(adminUri); - return doClean(client, uri, - vhost == null ? "/" : vhost, - binderPrefix == null ? BINDER_PREFIX : binderPrefix, entity, isJob); - } - catch (URISyntaxException e) { - throw new RabbitAdminException("Couldn't create a Client", e); - } - } - - private Map> doClean(WebClient client, - URI uri, String vhost, String binderPrefix, String entity, boolean isJob) { - - LinkedList removedQueues = isJob ? null - : findStreamQueues(client, uri, vhost, binderPrefix, entity); - List removedExchanges = findExchanges(client, uri, vhost, binderPrefix, entity); - // Delete the queues in reverse order to enable re-running after a partial - // success. - // The queue search above starts with 0 and terminates on a not found. - if (removedQueues != null) { - removedQueues.descendingIterator().forEachRemaining(q -> { - deleteQueue(client, uri, vhost, q); - if (logger.isDebugEnabled()) { - logger.debug("deleted queue: " + q); - } - }); - } - Map> results = new HashMap<>(); - if (removedQueues.size() > 0) { - results.put("queues", removedQueues); - } - // Fanout exchanges for taps - removedExchanges.forEach(exchange -> { - deleteExchange(client, uri, vhost, exchange); - if (logger.isDebugEnabled()) { - logger.debug("deleted exchange: " + exchange); - } - }); - if (removedExchanges.size() > 0) { - results.put("exchanges", removedExchanges); - } - return results; - } - - private void deleteQueue(WebClient client, URI uri, String vhost, String q) { - URI deleteURI = uri - .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + q); - client.delete() - .uri(deleteURI) - .retrieve() - .toEntity(Void.class) - .block(Duration.ofSeconds(10)); - } - - private void deleteExchange(WebClient client, URI uri, String vhost, String ex) { - URI deleteURI = uri - .resolve("/api/exchanges/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + ex); - client.delete() - .uri(deleteURI) - .retrieve() - .toEntity(Void.class) - .block(Duration.ofSeconds(10)); - } - - private LinkedList findStreamQueues(WebClient client, URI uri, String vhost, String binderPrefix, - String stream) { - - String queueNamePrefix = adjustPrefix(AbstractBinder.applyPrefix(binderPrefix, stream)); - List> queues = getQueues(client, uri, vhost); - return queues.stream() - .filter(q -> ((String) q.get("name")).startsWith(queueNamePrefix)) - .map(q -> checkNoConsumers(q)) - .collect(Collectors.toCollection(LinkedList::new)); - } - - private List> getQueues(WebClient client, URI uri, String vhost) { - URI getUri = uri - .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/"); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>>() { - }) - .block(Duration.ofSeconds(10)); - } - - private String adjustPrefix(String prefix) { - if (prefix.endsWith("*")) { - return prefix.substring(0, prefix.length() - 1); - } - else { - return prefix + PREFIX_DELIMITER; - } - } - - private String checkNoConsumers(Map queue) { - if ((Integer) queue.get("consumers") != 0) { - throw new RabbitAdminException("Queue " + queue.get("name") + " is in use"); - } - return (String) queue.get("name"); - } - - private List findExchanges(WebClient client, URI uri, String vhost, String binderPrefix, String entity) { - List> exchanges = getExchanges(client, uri, vhost); - String exchangeNamePrefix = adjustPrefix(AbstractBinder.applyPrefix(binderPrefix, entity)); - List exchangesToRemove = exchanges.stream() - .filter(e -> ((String) e.get("name")).startsWith(exchangeNamePrefix)) - .map(e -> { - List> bindingsBySource = - getBindingsBySource(client, uri, vhost, (String) e.get("name")); - return Collections.singletonMap((String) e.get("name"), bindingsBySource); - }) - .map(bindingsMap -> hasNoForeignBindings(bindingsMap, exchangeNamePrefix)) - .collect(Collectors.toList()); - exchangesToRemove.stream() - .map(exchange -> getExchangeBindingsByDestination(client, uri, vhost, exchange)) - .forEach(bindings -> { - if (bindings.size() > 0) { - throw new RabbitAdminException("Cannot delete exchange " - + bindings.get(0).get("destination") + "; it is a destination: " + bindings); - } - }); - return exchangesToRemove; - } - - private List> getExchangeBindingsByDestination(WebClient client, URI uri, String vhost, - String name) { - - String exchange = "".equals(name) ? "amq.default" : name; - URI getUri = uri - .resolve("/api/exchanges/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" - + UriUtils.encodePathSegment(exchange, StandardCharsets.UTF_8) + "/bindings/destination"); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>>() { - }) - .block(Duration.ofSeconds(10)); - } - - private List> getBindingsBySource(WebClient client, URI uri, String vhost, String name) { - String exchange = "".equals(name) ? "amq.default" : name; - URI getUri = uri - .resolve("/api/exchanges/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" - + UriUtils.encodePathSegment(exchange, StandardCharsets.UTF_8) + "/bindings/source"); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>>() { - }) - .block(Duration.ofSeconds(10)); - } - - private List> getExchanges(WebClient client, URI uri, String vhost) { - URI getUri = uri - .resolve("/api/exchanges/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/"); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>>() { - }) - .block(Duration.ofSeconds(10)); - } - - private String hasNoForeignBindings(Map>> bindings, String exchangeNamePrefix) { - Entry>> next = bindings.entrySet().iterator().next(); - for (Map binding : next.getValue()) { - if (!"queue".equals(binding.get("destination_type")) - || !((String) binding.get("destination")).startsWith(exchangeNamePrefix)) { - throw new RabbitAdminException("Cannot delete exchange " - + next.getKey() + "; it has bindings: " + bindings); - } - } - return next.getKey(); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBinderConfigurationProperties.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBinderConfigurationProperties.java deleted file mode 100644 index b13fe5c03..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBinderConfigurationProperties.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder.rabbit.properties; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * @author David Turanski - * @author Gary Russell - * @author Ben Blinebury - */ -@ConfigurationProperties(prefix = "spring.cloud.stream.rabbit.binder") -public class RabbitBinderConfigurationProperties { - - /** - * Urls for management plugins; only needed for queue affinity. - */ - private String[] adminAddresses = new String[0]; - - /** - * Cluster member node names; only needed for queue affinity. - */ - private String[] nodes = new String[0]; - - /** - * Compression level for compressed bindings; see 'java.util.zip.Deflator'. - */ - private Integer compressionLevel; - - /** - * Prefix for connection names from this binder. - */ - private String connectionNamePrefix; - - public String[] getAdminAddresses() { - return adminAddresses; - } - - public void setAdminAddresses(String[] adminAddresses) { - this.adminAddresses = adminAddresses; - } - - /** - * @param adminAddresses A comma-separated list of RabbitMQ management plugin URLs. - * @deprecated in favor of {@link #setAdminAddresses(String[])}. Will be removed in a - * future release. - */ - @Deprecated - public void setAdminAdresses(String[] adminAddresses) { - setAdminAddresses(adminAddresses); - } - - @Deprecated - public String[] getAdminAdresses() { - return this.adminAddresses; - } - - public String[] getNodes() { - return nodes; - } - - public void setNodes(String[] nodes) { - this.nodes = nodes; - } - - public Integer getCompressionLevel() { - return compressionLevel; - } - - public void setCompressionLevel(Integer compressionLevel) { - this.compressionLevel = compressionLevel; - } - - public String getConnectionNamePrefix() { - return this.connectionNamePrefix; - } - - public void setConnectionNamePrefix(String connectionNamePrefix) { - this.connectionNamePrefix = connectionNamePrefix; - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBindingProperties.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBindingProperties.java deleted file mode 100644 index b1e141c3d..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitBindingProperties.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.properties; - -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public class RabbitBindingProperties implements BinderSpecificPropertiesProvider { - - private RabbitConsumerProperties consumer = new RabbitConsumerProperties(); - - private RabbitProducerProperties producer = new RabbitProducerProperties(); - - public RabbitConsumerProperties getConsumer() { - return consumer; - } - - public void setConsumer(RabbitConsumerProperties consumer) { - this.consumer = consumer; - } - - public RabbitProducerProperties getProducer() { - return producer; - } - - public void setProducer(RabbitProducerProperties producer) { - this.producer = producer; - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitCommonProperties.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitCommonProperties.java deleted file mode 100644 index b72dcd91d..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitCommonProperties.java +++ /dev/null @@ -1,588 +0,0 @@ -/* - * Copyright 2017-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.properties; - -import java.util.HashMap; -import java.util.Map; - -import org.hibernate.validator.constraints.Range; - -import org.springframework.amqp.core.ExchangeTypes; - -/** - * @author Gary Russell - * @author Soby Chacko - * @since 1.2 - * - */ -public abstract class RabbitCommonProperties { - - /** - * DLQ name prefix. - */ - public static final String DEAD_LETTER_EXCHANGE = "DLX"; - - /** - * type of exchange to declare (if necessary, and declareExchange is true). - */ - private String exchangeType = ExchangeTypes.TOPIC; - - /** - * whether to declare the exchange. - */ - private boolean declareExchange = true; - - /** - * whether to declare the exchange as durable. - */ - private boolean exchangeDurable = true; - - /** - * whether to declare the exchange as auto-delete. - */ - private boolean exchangeAutoDelete = false; - - /** - * whether a delayed message exchange should be used. - */ - private boolean delayedExchange = false; - - /** - * set to true to name the queue with only the group; default is destination.group. - */ - private boolean queueNameGroupOnly = false; - - /** - * whether to bind a queue (or queues when partitioned) to the exchange. - */ - private boolean bindQueue = true; - - /** - * routing key to bind (default # for non-partitioned, destination-instanceIndex for - * partitioned). - */ - private String bindingRoutingKey; - - /** - * when not null, treat 'bindingRoutingKey' as a delimited list of keys to bind. - */ - private String bindingRoutingKeyDelimiter; - - /** - * default time to live to apply to the queue when declared (ms). - */ - private Integer ttl; - - /** - * how long before an unused queue is deleted (ms). - */ - private Integer expires; - - /** - * maximum number of messages in the queue. - */ - private Integer maxLength; - - /** - * maximum number of total bytes in the queue from all messages. - */ - private Integer maxLengthBytes; - - /** - * maximum priority of messages in the queue (0-255). - */ - private Integer maxPriority; - - /** - * name of the DLQ - default is prefix+destination.dlq. - */ - private String deadLetterQueueName; - - /** - * a DLX to assign to the queue; if autoBindDlq is true, defaults to 'prefix+DLX'. - */ - private String deadLetterExchange; - - /** - * the type of the DLX, if autoBindDlq is true. - */ - private String deadLetterExchangeType = ExchangeTypes.DIRECT; - - /** - * whether to declare the dead-letter exchange when autoBindDlq is true. - */ - private boolean declareDlx = true; - - /** - * a dead letter routing key to assign to that queue; if autoBindDlq is true, defaults - * to destination. - */ - private String deadLetterRoutingKey; - - /** - * default time to live to apply to the dead letter queue when declared (ms). - */ - private Integer dlqTtl; - - /** - * how long before an unused dead letter queue is deleted (ms). - */ - private Integer dlqExpires; - - /** - * maximum number of messages in the dead letter queue. - */ - private Integer dlqMaxLength; - - /** - * maximum number of total bytes in the dead letter queue from all messages. - */ - private Integer dlqMaxLengthBytes; - - /** - * maximum priority of messages in the dead letter queue (0-255). - */ - private Integer dlqMaxPriority; - - /** - * if a DLQ is declared, a DLX to assign to that queue; default none. - */ - private String dlqDeadLetterExchange; - - /** - * if a DLQ is declared, a dead letter routing key to assign to that queue; default - * none. - */ - private String dlqDeadLetterRoutingKey; - - /** - * true to automatically bind a dead letter queue to a DLX. - */ - private boolean autoBindDlq; - - /** - * prefix for elements declared in RabbitMQ (exchanges, queues). - */ - private String prefix = ""; - - /** - * true if the queue is provisioned as a lazy queue. - */ - private boolean lazy; - - /** - * true if the DLQ is provisioned as a lazy queue. - */ - private boolean dlqLazy; - - /** - * action when maxLength or maxLengthBytes is exceeded. - */ - private String overflowBehavior; - - /** - * action when maxLength or maxLengthBytes is exceeded. - */ - private String dlqOverflowBehavior; - - /** - * A map of binding arguments to apply when binding the queue to the exchange. - * Useful for a headers exchange, for example. - */ - private Map queueBindingArguments = new HashMap<>(); - - /** - * A map of binding arguments to apply when binding the dlq to the exchange. - * Useful for a headers exchange, for example. - */ - private Map dlqBindingArguments = new HashMap<>(); - - /** - * Configure the queue to be type quorum instead of classic. - */ - private QuorumConfig quorum = new QuorumConfig(); - - /** - * Configure the DLQ to be type quorum instead of classic. - */ - private QuorumConfig dlqQuorum = new QuorumConfig(); - - /** - * When true, set the 'x-single-active-consumer' queue argument to true. - */ - private boolean singleActiveConsumer; - - /** - * When true, set the 'x-single-active-consumer' queue argument to true. - */ - private boolean dlqSingleActiveConsumer; - - /** - * The bean name of a stream message converter to convert from a Spring AMQP Message - * to a Stream Message. - * @since 3.2 - */ - private String streamStreamMessageConverterBeanName; - - public String getExchangeType() { - return this.exchangeType; - } - - public void setExchangeType(String exchangeType) { - this.exchangeType = exchangeType; - } - - public boolean isDeclareExchange() { - return this.declareExchange; - } - - public void setDeclareExchange(boolean declareExchange) { - this.declareExchange = declareExchange; - } - - public boolean isExchangeDurable() { - return this.exchangeDurable; - } - - public void setExchangeDurable(boolean exchangeDurable) { - this.exchangeDurable = exchangeDurable; - } - - public boolean isExchangeAutoDelete() { - return this.exchangeAutoDelete; - } - - public void setExchangeAutoDelete(boolean exchangeAutoDelete) { - this.exchangeAutoDelete = exchangeAutoDelete; - } - - public boolean isDelayedExchange() { - return this.delayedExchange; - } - - public void setDelayedExchange(boolean delayedExchange) { - this.delayedExchange = delayedExchange; - } - - public boolean isQueueNameGroupOnly() { - return this.queueNameGroupOnly; - } - - public void setQueueNameGroupOnly(boolean queueNameGroupOnly) { - this.queueNameGroupOnly = queueNameGroupOnly; - } - - public boolean isBindQueue() { - return this.bindQueue; - } - - public void setBindQueue(boolean bindQueue) { - this.bindQueue = bindQueue; - } - - public String getBindingRoutingKey() { - return this.bindingRoutingKey; - } - - public void setBindingRoutingKey(String routingKey) { - this.bindingRoutingKey = routingKey; - } - - public String getBindingRoutingKeyDelimiter() { - return this.bindingRoutingKeyDelimiter; - } - - public void setBindingRoutingKeyDelimiter(String bindingRoutingKeyDelimiter) { - this.bindingRoutingKeyDelimiter = bindingRoutingKeyDelimiter; - } - - public Integer getTtl() { - return this.ttl; - } - - public void setTtl(Integer ttl) { - this.ttl = ttl; - } - - public Integer getExpires() { - return this.expires; - } - - public void setExpires(Integer expires) { - this.expires = expires; - } - - public Integer getMaxLength() { - return this.maxLength; - } - - public void setMaxLength(Integer maxLength) { - this.maxLength = maxLength; - } - - public Integer getMaxLengthBytes() { - return this.maxLengthBytes; - } - - public void setMaxLengthBytes(Integer maxLengthBytes) { - this.maxLengthBytes = maxLengthBytes; - } - - @Range(min = 0, max = 255) - public Integer getMaxPriority() { - return this.maxPriority; - } - - public void setMaxPriority(Integer maxPriority) { - this.maxPriority = maxPriority; - } - - public String getDeadLetterQueueName() { - return this.deadLetterQueueName; - } - - public void setDeadLetterQueueName(String deadLetterQueueName) { - this.deadLetterQueueName = deadLetterQueueName; - } - - public String getDeadLetterExchange() { - return this.deadLetterExchange; - } - - public void setDeadLetterExchange(String deadLetterExchange) { - this.deadLetterExchange = deadLetterExchange; - } - - public String getDeadLetterExchangeType() { - return this.deadLetterExchangeType; - } - - public void setDeadLetterExchangeType(String deadLetterExchangeType) { - this.deadLetterExchangeType = deadLetterExchangeType; - } - - public boolean isDeclareDlx() { - return this.declareDlx; - } - - public void setDeclareDlx(boolean declareDlx) { - this.declareDlx = declareDlx; - } - - public String getDeadLetterRoutingKey() { - return this.deadLetterRoutingKey; - } - - public void setDeadLetterRoutingKey(String deadLetterRoutingKey) { - this.deadLetterRoutingKey = deadLetterRoutingKey; - } - - public Integer getDlqTtl() { - return this.dlqTtl; - } - - public void setDlqTtl(Integer dlqTtl) { - this.dlqTtl = dlqTtl; - } - - public Integer getDlqExpires() { - return this.dlqExpires; - } - - public void setDlqExpires(Integer dlqExpires) { - this.dlqExpires = dlqExpires; - } - - public Integer getDlqMaxLength() { - return this.dlqMaxLength; - } - - public void setDlqMaxLength(Integer dlqMaxLength) { - this.dlqMaxLength = dlqMaxLength; - } - - public Integer getDlqMaxLengthBytes() { - return this.dlqMaxLengthBytes; - } - - public void setDlqMaxLengthBytes(Integer dlqMaxLengthBytes) { - this.dlqMaxLengthBytes = dlqMaxLengthBytes; - } - - public Integer getDlqMaxPriority() { - return this.dlqMaxPriority; - } - - public void setDlqMaxPriority(Integer dlqMaxPriority) { - this.dlqMaxPriority = dlqMaxPriority; - } - - public String getDlqDeadLetterExchange() { - return this.dlqDeadLetterExchange; - } - - public void setDlqDeadLetterExchange(String dlqDeadLetterExchange) { - this.dlqDeadLetterExchange = dlqDeadLetterExchange; - } - - public String getDlqDeadLetterRoutingKey() { - return this.dlqDeadLetterRoutingKey; - } - - public void setDlqDeadLetterRoutingKey(String dlqDeadLetterRoutingKey) { - this.dlqDeadLetterRoutingKey = dlqDeadLetterRoutingKey; - } - - public boolean isAutoBindDlq() { - return autoBindDlq; - } - - public void setAutoBindDlq(boolean autoBindDlq) { - this.autoBindDlq = autoBindDlq; - } - - public String getPrefix() { - return prefix; - } - - public void setPrefix(String prefix) { - this.prefix = prefix; - } - - public boolean isLazy() { - return this.lazy; - } - - public void setLazy(boolean lazy) { - this.lazy = lazy; - } - - public boolean isDlqLazy() { - return this.dlqLazy; - } - - public void setDlqLazy(boolean dlqLazy) { - this.dlqLazy = dlqLazy; - } - - public String getOverflowBehavior() { - return this.overflowBehavior; - } - - public void setOverflowBehavior(String overflowBehavior) { - this.overflowBehavior = overflowBehavior; - } - - public String getDlqOverflowBehavior() { - return this.dlqOverflowBehavior; - } - - public void setDlqOverflowBehavior(String dlqOverflowBehavior) { - this.dlqOverflowBehavior = dlqOverflowBehavior; - } - - public Map getQueueBindingArguments() { - return this.queueBindingArguments; - } - - public void setQueueBindingArguments(Map queueBindingArguments) { - this.queueBindingArguments = queueBindingArguments; - } - - public Map getDlqBindingArguments() { - return this.dlqBindingArguments; - } - - public void setDlqBindingArguments(Map dlqBindingArguments) { - this.dlqBindingArguments = dlqBindingArguments; - } - - public QuorumConfig getQuorum() { - return this.quorum; - } - - public void setQuorum(QuorumConfig quorum) { - this.quorum = quorum; - } - - public QuorumConfig getDlqQuorum() { - return this.dlqQuorum; - } - - public void setDlqQuorum(QuorumConfig dlqQuorum) { - this.dlqQuorum = dlqQuorum; - } - - public boolean isSingleActiveConsumer() { - return this.singleActiveConsumer; - } - - public void setSingleActiveConsumer(boolean singleActiveConsumer) { - this.singleActiveConsumer = singleActiveConsumer; - } - - public boolean isDlqSingleActiveConsumer() { - return this.dlqSingleActiveConsumer; - } - - public void setDlqSingleActiveConsumer(boolean dlqSingleActiveConsumer) { - this.dlqSingleActiveConsumer = dlqSingleActiveConsumer; - } - - public String getStreamStreamMessageConverterBeanName() { - return this.streamStreamMessageConverterBeanName; - } - - public void setStreamStreamMessageConverterBeanName(String streamStreamMessageConverterBeanName) { - this.streamStreamMessageConverterBeanName = streamStreamMessageConverterBeanName; - } - - public static class QuorumConfig { - - private boolean enabled; - - private Integer initialGroupSize; - - private Integer deliveryLimit; - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Integer getInitialGroupSize() { - return this.initialGroupSize; - } - - public void setInitialGroupSize(Integer initialGroupSize) { - this.initialGroupSize = initialGroupSize; - } - - public Integer getDeliveryLimit() { - return this.deliveryLimit; - } - - public void setDeliveryLimit(Integer deliveryLimit) { - this.deliveryLimit = deliveryLimit; - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitConsumerProperties.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitConsumerProperties.java deleted file mode 100644 index 25ae65a84..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitConsumerProperties.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.properties; - -import jakarta.validation.constraints.Min; - -import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.amqp.core.MessageDeliveryMode; -import org.springframework.util.Assert; - -/** - * @author Marius Bogoevici - * @author Gary Russell - */ -public class RabbitConsumerProperties extends RabbitCommonProperties { - - /** - * true to use transacted channels. - */ - private boolean transacted; - - /** - * container acknowledge mode. - */ - private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; - - /** - * maxumum concurrency of this consumer (threads). - */ - private int maxConcurrency = 1; - - /** - * number of prefetched messages pre consumer thread. - */ - private int prefetch = 1; - - /** - * messages per acknowledgment (and commit when transacted). - */ - private int batchSize = 1; - - /** - * true for a durable subscription. - */ - private boolean durableSubscription = true; - - /** - * republish failures to the DLQ with diagnostic headers. - */ - private boolean republishToDlq = true; - - /** - * when republishing to the DLQ, the delivery mode to use. - */ - private MessageDeliveryMode republishDeliveyMode = MessageDeliveryMode.PERSISTENT; - - /** - * true to requeue rejected messages, false to discard (or route to DLQ). - */ - private boolean requeueRejected = false; - - /** - * patterns to match which headers are mapped (inbound). - */ - private String[] headerPatterns = new String[] { "*" }; - - /** - * interval between reconnection attempts. - */ - private long recoveryInterval = 5000; - - /** - * true if the consumer is exclusive. - */ - private boolean exclusive; - - /** - * when true, stop the container instead of retrying queue declarations. - */ - private boolean missingQueuesFatal = false; - - /** - * how many times to attempt passive queue declaration. - */ - private Integer queueDeclarationRetries; - - /** - * interval between attempts to passively declare missing queues. - */ - private Long failedDeclarationRetryInterval; - - /** - * Used to create the consumer tags; will be appended by '#n' where 'n' increments for - * each consumer created. - */ - private String consumerTagPrefix; - - /** - * Room to leave for other headers after adding the stack trace to a DLQ message. - */ - private int frameMaxHeadroom = 20_000; - - /** - * The container type, SIMPLE, DIRECT, or STREAM. - */ - private ContainerType containerType = ContainerType.SIMPLE; - - /** - * Prefix for anonymous queue names (when no group is provided). - */ - private String anonymousGroupPrefix = "anonymous."; - - /** - * When true, the listener container will assemble a list from multiple messages, - * according to the batchSize and receiveTimeout properties. Only applies with - * {@link ContainerType#SIMPLE}. - */ - private boolean enableBatching; - - /** - * How long to block waiting to receive messages; increasing from the default 1 second - * will make the binding less responsive to stop requests. When enableConsumerBatching - * is true, a short batch may be emitted if this time elapses before the batchSize is - * satisfied. Only applies with {@link ContainerType#SIMPLE}. - */ - private Long receiveTimeout; - - /** - * When the container type is STREAM, set this to true to create a super stream with - * competing consumers. - */ - private boolean superStream; - - public boolean isTransacted() { - return transacted; - } - - public void setTransacted(boolean transacted) { - this.transacted = transacted; - } - - public AcknowledgeMode getAcknowledgeMode() { - return acknowledgeMode; - } - - public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { - Assert.notNull(acknowledgeMode, "Acknowledge mode cannot be null"); - this.acknowledgeMode = acknowledgeMode; - } - - @Min(value = 1, message = "Max Concurrency should be greater than zero.") - public int getMaxConcurrency() { - return maxConcurrency; - } - - public void setMaxConcurrency(int maxConcurrency) { - this.maxConcurrency = maxConcurrency; - } - - @Min(value = 1, message = "Prefetch should be greater than zero.") - public int getPrefetch() { - return prefetch; - } - - public void setPrefetch(int prefetch) { - this.prefetch = prefetch; - } - - /** - * @return the header patterns. - * @deprecated - use {@link #getHeaderPatterns()}. - */ - @Deprecated - public String[] getRequestHeaderPatterns() { - return this.headerPatterns; - } - - /** - * @param requestHeaderPatterns request header patterns - * @deprecated - use {@link #setHeaderPatterns(String[])}. - */ - @Deprecated - public void setRequestHeaderPatterns(String[] requestHeaderPatterns) { - this.headerPatterns = requestHeaderPatterns; - } - - /** - * @return the tx size. - * @deprecated in favor of {@link #getBatchSize()} - */ - @Deprecated - @Min(value = 1, message = "Tx Size should be greater than zero.") - public int getTxSize() { - return getBatchSize(); - } - - /** - * @param txSize the tx size - * deprecated in favor of {@link #setBatchSize(int)}. - */ - public void setTxSize(int txSize) { - setBatchSize(txSize); - } - - @Min(value = 1, message = "Batch Size should be greater than zero.") - public int getBatchSize() { - return batchSize; - } - - public void setBatchSize(int batchSize) { - this.batchSize = batchSize; - } - - public boolean isDurableSubscription() { - return durableSubscription; - } - - public void setDurableSubscription(boolean durableSubscription) { - this.durableSubscription = durableSubscription; - } - - public boolean isRepublishToDlq() { - return republishToDlq; - } - - public void setRepublishToDlq(boolean republishToDlq) { - this.republishToDlq = republishToDlq; - } - - public boolean isRequeueRejected() { - return requeueRejected; - } - - public MessageDeliveryMode getRepublishDeliveyMode() { - return this.republishDeliveyMode; - } - - public void setRepublishDeliveyMode(MessageDeliveryMode republishDeliveyMode) { - this.republishDeliveyMode = republishDeliveyMode; - } - - public void setRequeueRejected(boolean requeueRejected) { - this.requeueRejected = requeueRejected; - } - - public String[] getHeaderPatterns() { - return headerPatterns; - } - - public void setHeaderPatterns(String[] replyHeaderPatterns) { - this.headerPatterns = replyHeaderPatterns; - } - - public long getRecoveryInterval() { - return recoveryInterval; - } - - public void setRecoveryInterval(long recoveryInterval) { - this.recoveryInterval = recoveryInterval; - } - - public boolean isExclusive() { - return this.exclusive; - } - - public void setExclusive(boolean exclusive) { - this.exclusive = exclusive; - } - - public boolean getMissingQueuesFatal() { - return this.missingQueuesFatal; - } - - public void setMissingQueuesFatal(boolean missingQueuesFatal) { - this.missingQueuesFatal = missingQueuesFatal; - } - - public Integer getQueueDeclarationRetries() { - return this.queueDeclarationRetries; - } - - public void setQueueDeclarationRetries(Integer queueDeclarationRetries) { - this.queueDeclarationRetries = queueDeclarationRetries; - } - - public Long getFailedDeclarationRetryInterval() { - return this.failedDeclarationRetryInterval; - } - - public void setFailedDeclarationRetryInterval(Long failedDeclarationRetryInterval) { - this.failedDeclarationRetryInterval = failedDeclarationRetryInterval; - } - - public String getConsumerTagPrefix() { - return this.consumerTagPrefix; - } - - public void setConsumerTagPrefix(String consumerTagPrefix) { - this.consumerTagPrefix = consumerTagPrefix; - } - - public int getFrameMaxHeadroom() { - return this.frameMaxHeadroom; - } - - public void setFrameMaxHeadroom(int frameMaxHeadroom) { - this.frameMaxHeadroom = frameMaxHeadroom; - } - - public ContainerType getContainerType() { - return this.containerType; - } - - public void setContainerType(ContainerType containerType) { - this.containerType = containerType; - } - - public String getAnonymousGroupPrefix() { - return this.anonymousGroupPrefix; - } - - public void setAnonymousGroupPrefix(String anonymousGroupPrefix) { - this.anonymousGroupPrefix = anonymousGroupPrefix; - } - - public boolean isEnableBatching() { - return this.enableBatching; - } - - public void setEnableBatching(boolean enableBatching) { - this.enableBatching = enableBatching; - } - - public Long getReceiveTimeout() { - return this.receiveTimeout; - } - - public void setReceiveTimeout(Long receiveTimeout) { - this.receiveTimeout = receiveTimeout; - } - - public boolean isSuperStream() { - return this.superStream; - } - - public void setSuperStream(boolean superStream) { - this.superStream = superStream; - } - - /** - * Container type. - * @author Gary Russell - * @since 3.2 - * - */ - public enum ContainerType { - - /** - * Container where the RabbitMQ consumer dispatches messages to an invoker thread. - */ - SIMPLE, - - /** - * Container where the listener is invoked directly on the RabbitMQ consumer - * thread. - */ - DIRECT, - - /** - * Container that uses the RabbitMQ Stream Client. - */ - STREAM - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitExtendedBindingProperties.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitExtendedBindingProperties.java deleted file mode 100644 index 80d6b6530..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitExtendedBindingProperties.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.properties; - -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * @author Marius Bogoevici - * @author Gary Russell - * @author Oleg Zhurakousky - * @author Soby Chacko - */ -@ConfigurationProperties("spring.cloud.stream.rabbit") -public class RabbitExtendedBindingProperties extends - AbstractExtendedBindingProperties { - - private static final String DEFAULTS_PREFIX = "spring.cloud.stream.rabbit.default"; - - @Override - public String getDefaultsPrefix() { - return DEFAULTS_PREFIX; - } - - public Map getBindings() { - return this.doGetBindings(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return RabbitBindingProperties.class; - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java deleted file mode 100644 index 678eb7c9d..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/properties/RabbitProducerProperties.java +++ /dev/null @@ -1,423 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.properties; - -import java.util.Optional; - -import jakarta.validation.constraints.Min; - -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.core.MessageDeliveryMode; -import org.springframework.expression.Expression; -import org.springframework.expression.common.LiteralExpression; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - - - -/** - * @author Marius Bogoevici - * @author Gary Russell - */ -public class RabbitProducerProperties extends RabbitCommonProperties { - - /** - * Determines the producer type. - * @since 3.2 - */ - public enum ProducerType { - - /** - * RabbitMQ Stream producer - blocks until confirm received. - */ - STREAM_SYNC, - - /** - * RabbitMQ Stream producer - does not block. - */ - STREAM_ASYNC, - - /** - * Classic AMQP producer. - */ - AMQP - } - - /** - * true to compress messages. - */ - private boolean compress; - - /** - * true to batch multiple messages into one. - */ - private boolean batchingEnabled; - - /** - * the number of messages to batch, when enabled. - */ - private int batchSize = 100; - - /** - * the size limit for batched messages. - */ - private int batchBufferLimit = 10000; - - /** - * the time after which an incomplete batch will be sent. - */ - private int batchTimeout = 5000; - - /** - * the bean name of a custom batching strategy to use instead of the - * {@link org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy}. - */ - private String batchingStrategyBeanName; - - /** - * true to use transacted channels. - */ - private boolean transacted; - - /** - * the delivery mode for published messages. - */ - private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT; - - /** - * patterns to match which headers are mapped (inbound). - */ - private String[] headerPatterns = new String[] { "*" }; - - /** - * when using a delayed message exchange, a SpEL expression to determine the delay to - * apply to messages. - */ - private Expression delayExpression; - - /** - * a static routing key when publishing messages; default is the destination name; - * suffixed by "-partition" when partitioned. This is only used if `routingKeyExpression` is null - */ - private String routingKey; - - /** - * a custom routing key when publishing messages; default is the destination name; - * suffixed by "-partition" when partitioned. - */ - private Expression routingKeyExpression; - - /** - * the channel name to which to send publisher confirms (acks) if the connection - * factory is so configured; default 'nullChannel'; requires - * 'errorChannelEnabled=true'. - */ - private String confirmAckChannel; - - /** - * When true, the binding will complete the {@link java.util.concurrent.Future} field - * in a {@link org.springframework.amqp.rabbit.connection.CorrelationData} contained - * in the - * {@link org.springframework.amqp.support.AmqpHeaders#PUBLISH_CONFIRM_CORRELATION} - * header when the confirmation is received. - */ - private boolean useConfirmHeader; - - /** - * When STREAM_SYNC or STREAM_ASYNC, create a RabbitMQ Stream producer instead of an - * AMQP producer. - * @since 3.2 - */ - private ProducerType producerType = ProducerType.AMQP; - - /** - * The bean name of a message converter to convert from spring-messaging Message to - * a Spring AMQP Message. - * @since 3.2 - */ - private String streamMessageConverterBeanName; - - /** - * Configure an alternate exchange for when no queues are bound. - * @since 4.0 - */ - private AlternateExchange alternateExchange; - - /** - * When the producer type is STREAM_*, set this to true to publish to a super stream. - * Also requires a partition key. - */ - private boolean superStream; - - /** - * @param requestHeaderPatterns the patterns. - * @deprecated - use {@link #setHeaderPatterns(String[])}. - */ - @Deprecated - public void setRequestHeaderPatterns(String[] requestHeaderPatterns) { - this.headerPatterns = requestHeaderPatterns; - } - - /** - * @return the header patterns. - * @deprecated - use {@link #getHeaderPatterns()}. - */ - @Deprecated - public String[] getRequestHeaderPatterns() { - return this.headerPatterns; - } - - public void setCompress(boolean compress) { - this.compress = compress; - } - - public boolean isCompress() { - return compress; - } - - public void setDeliveryMode(MessageDeliveryMode deliveryMode) { - this.deliveryMode = deliveryMode; - } - - public MessageDeliveryMode getDeliveryMode() { - return deliveryMode; - } - - public String[] getHeaderPatterns() { - return headerPatterns; - } - - public void setHeaderPatterns(String[] replyHeaderPatterns) { - this.headerPatterns = replyHeaderPatterns; - } - - public boolean isBatchingEnabled() { - return batchingEnabled; - } - - public void setBatchingEnabled(boolean batchingEnabled) { - this.batchingEnabled = batchingEnabled; - } - - @Min(value = 1, message = "Batch Size should be greater than zero.") - public int getBatchSize() { - return batchSize; - } - - public void setBatchSize(int batchSize) { - this.batchSize = batchSize; - } - - @Min(value = 1, message = "Batch Buffer Limit should be greater than zero.") - public int getBatchBufferLimit() { - return batchBufferLimit; - } - - public void setBatchBufferLimit(int batchBufferLimit) { - this.batchBufferLimit = batchBufferLimit; - } - - @Min(value = 1, message = "Batch Timeout should be greater than zero.") - public int getBatchTimeout() { - return batchTimeout; - } - - public void setBatchTimeout(int batchTimeout) { - this.batchTimeout = batchTimeout; - } - - public boolean isTransacted() { - return this.transacted; - } - - public void setTransacted(boolean transacted) { - this.transacted = transacted; - } - - public Expression getDelayExpression() { - return this.delayExpression; - } - - public void setDelayExpression(Expression delayExpression) { - this.delayExpression = delayExpression; - } - - public Expression getRoutingKeyExpression() { - return Optional.ofNullable(this.routingKeyExpression) - .orElseGet(() -> Optional.ofNullable(this.routingKey) - .map(LiteralExpression::new) - .orElse(null)); - } - - public void setRoutingKeyExpression(Expression routingKeyExpression) { - this.routingKeyExpression = routingKeyExpression; - } - - public String getRoutingKey() { - return this.routingKey; - } - - public void setRoutingKey(String routingKey) { - this.routingKey = routingKey; - } - - public String getConfirmAckChannel() { - return this.confirmAckChannel; - } - - public void setConfirmAckChannel(String confirmAckChannel) { - this.confirmAckChannel = confirmAckChannel; - } - - public String getBatchingStrategyBeanName() { - return batchingStrategyBeanName; - } - - public void setBatchingStrategyBeanName(String batchingStrategyBeanName) { - this.batchingStrategyBeanName = batchingStrategyBeanName; - } - - public boolean isUseConfirmHeader() { - return this.useConfirmHeader; - } - - public void setUseConfirmHeader(boolean useConfirmHeader) { - this.useConfirmHeader = useConfirmHeader; - } - - public ProducerType getProducerType() { - return this.producerType; - } - - public void setProducerType(ProducerType producerType) { - Assert.notNull(producerType, "'producerType' cannot be null"); - this.producerType = producerType; - } - - public String getStreamMessageConverterBeanName() { - return this.streamMessageConverterBeanName; - } - - public void setStreamMessageConverterBeanName(String streamMessageConverterBeanName) { - this.streamMessageConverterBeanName = streamMessageConverterBeanName; - } - - @Nullable - public AlternateExchange getAlternateExchange() { - return this.alternateExchange; - } - - public void setAlternateExchange(AlternateExchange alternate) { - this.alternateExchange = alternate; - } - - public boolean isSuperStream() { - return this.superStream; - } - - public void setSuperStream(boolean superStream) { - this.superStream = superStream; - } - - public static class AlternateExchange { - - /** - * The alternate exchange name. - */ - private String name; - - /** - * Whether the exchange exists or should be provisioned. - */ - private boolean exists = false; - - /** - * The alternate exchange type. - */ - private String type = ExchangeTypes.TOPIC; - - /** - * Bind a durable queue to the alternate exchange. - */ - private Binding binding; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public boolean isExists() { - return this.exists; - } - - public void setExists(boolean exists) { - this.exists = exists; - } - - public String getType() { - return this.type; - } - - public void setType(String type) { - this.type = type; - } - - public Binding getBinding() { - return this.binding; - } - - public void setBinding(Binding binding) { - this.binding = binding; - } - - public static class Binding { - - /** - * The routing key. - */ - private String routingKey = "#"; - - /** - * The queue name. - */ - private String queue; - - public String getRoutingKey() { - return this.routingKey; - } - - public void setRoutingKey(String routingKey) { - this.routingKey = routingKey; - } - - public String getQueue() { - return this.queue; - } - - public void setQueue(String queue) { - this.queue = queue; - } - - - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java deleted file mode 100644 index 359a75d6a..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/main/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisioner.java +++ /dev/null @@ -1,946 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.provisioning; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.amqp.AmqpConnectException; -import org.springframework.amqp.core.AnonymousQueue; -import org.springframework.amqp.core.Base64UrlNamingStrategy; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.Binding.DestinationType; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Declarable; -import org.springframework.amqp.core.DeclarableCustomizer; -import org.springframework.amqp.core.Declarables; -import org.springframework.amqp.core.DirectExchange; -import org.springframework.amqp.core.Exchange; -import org.springframework.amqp.core.ExchangeBuilder; -import org.springframework.amqp.core.FanoutExchange; -import org.springframework.amqp.core.HeadersExchange; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.TopicExchange; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.DeclarationExceptionEvent; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties.QuorumConfig; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.AlternateExchange; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.cloud.stream.provisioning.ProvisioningException; -import org.springframework.cloud.stream.provisioning.ProvisioningProvider; -import org.springframework.context.ApplicationListener; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.lang.Nullable; -import org.springframework.rabbit.stream.config.SuperStream; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * AMQP implementation for {@link ProvisioningProvider}. - * - * @author Soby Chacko - * @author Gary Russell - * @author Oleg Zhurakousky - * @author Michael Michailidis - * @author Byungjun You - */ -// @checkstyle:off -public class RabbitExchangeQueueProvisioner - implements ApplicationListener, - ProvisioningProvider, ExtendedProducerProperties> { - - // @checkstyle:on - - /** - * The delimiter between a group and index when constructing a binder - * consumer/producer. - */ - private static final String GROUP_INDEX_DELIMITER = "."; - - protected final Log logger = LogFactory.getLog(getClass()); - - private final RabbitAdmin rabbitAdmin; - - private boolean notOurAdminException; - - private final GenericApplicationContext autoDeclareContext = new GenericApplicationContext(); - - private final List customizers; - - private final AtomicInteger producerExchangeBeanNameQualifier = new AtomicInteger(); - - public RabbitExchangeQueueProvisioner(ConnectionFactory connectionFactory) { - this(connectionFactory, Collections.emptyList()); - } - - public RabbitExchangeQueueProvisioner(ConnectionFactory connectionFactory, - List customizers) { - - this.rabbitAdmin = new RabbitAdmin(connectionFactory); - this.autoDeclareContext.refresh(); - this.rabbitAdmin.setApplicationContext(this.autoDeclareContext); - this.rabbitAdmin.afterPropertiesSet(); - this.customizers = customizers; - } - - @Override - public ProducerDestination provisionProducerDestination(String name, - ExtendedProducerProperties producerProperties) { - final String exchangeName = applyPrefix( - producerProperties.getExtension().getPrefix(), name); - String beanNameQualifier = "prod" + this.producerExchangeBeanNameQualifier.incrementAndGet(); - Exchange exchange = buildExchange(producerProperties.getExtension(), - exchangeName, producerProperties.getExtension().getAlternateExchange(), beanNameQualifier); - if (producerProperties.getExtension().isDeclareExchange()) { - declareExchange(exchangeName, beanNameQualifier, exchange); - } - Binding binding = null; - for (String requiredGroupName : producerProperties.getRequiredGroups()) { - String baseQueueName = producerProperties.getExtension() - .isQueueNameGroupOnly() ? requiredGroupName - : (exchangeName + "." + requiredGroupName); - if (!producerProperties.isPartitioned()) { - autoBindDLQ(baseQueueName, baseQueueName, requiredGroupName, - producerProperties.getExtension()); - if (producerProperties.getExtension().isBindQueue()) { - Queue queue = new Queue(baseQueueName, true, false, false, queueArgs( - baseQueueName, producerProperties.getExtension(), false)); - declareQueue(baseQueueName, queue); - String[] routingKeys = bindingRoutingKeys(producerProperties.getExtension()); - if (ObjectUtils.isEmpty(routingKeys)) { - binding = notPartitionedBinding(exchange, queue, null, producerProperties.getExtension()); - } - else { - for (String routingKey : routingKeys) { - binding = notPartitionedBinding(exchange, queue, routingKey, - producerProperties.getExtension()); - } - } - } - } - else { - // if the stream is partitioned, create one queue for each target - // partition for the default group - for (int i = 0; i < producerProperties.getPartitionCount(); i++) { - String partitionSuffix = "-" + i; - String partitionQueueName = baseQueueName + partitionSuffix; - autoBindDLQ(baseQueueName, baseQueueName + partitionSuffix, requiredGroupName, - producerProperties.getExtension()); - if (producerProperties.getExtension().isBindQueue()) { - Queue queue = new Queue(partitionQueueName, true, false, false, - queueArgs(partitionQueueName, - producerProperties.getExtension(), false)); - declareQueue(queue.getName(), queue); - String prefix = producerProperties.getExtension().getPrefix(); - String destination = !StringUtils.hasText(prefix) ? exchangeName - : exchangeName.substring(prefix.length()); - String[] routingKeys = bindingRoutingKeys(producerProperties.getExtension()); - if (ObjectUtils.isEmpty(routingKeys)) { - binding = partitionedBinding(destination, exchange, queue, null, - producerProperties.getExtension(), i); - } - else { - for (String routingKey : routingKeys) { - binding = partitionedBinding(destination, exchange, queue, routingKey, - producerProperties.getExtension(), i); - } - } - } - } - } - } - return new RabbitProducerDestination(exchange, binding, beanNameQualifier); - } - - @Override - public ConsumerDestination provisionConsumerDestination(String name, String group, - ExtendedConsumerProperties properties) { - ConsumerDestination consumerDestination; - if (!properties.isMultiplex()) { - consumerDestination = doProvisionConsumerDestination(name, group, properties); - } - else { - String[] provisionedDestinations = Stream - .of(StringUtils.tokenizeToStringArray(name, ",", true, true)) - .flatMap(destination -> { - if (properties.isPartitioned() && !ObjectUtils.isEmpty(properties.getInstanceIndexList())) { - List consumerDestinationNames = new ArrayList<>(); - - for (Integer index : properties.getInstanceIndexList()) { - ExtendedConsumerProperties temporaryProperties = - new ExtendedConsumerProperties<>(properties.getExtension()); - BeanUtils.copyProperties(properties, temporaryProperties); - temporaryProperties.setInstanceIndex(index); - consumerDestinationNames.add(doProvisionConsumerDestination(destination, group, - temporaryProperties).getName()); - } - - return consumerDestinationNames.stream(); - } - else { - return Stream.of(doProvisionConsumerDestination(destination, group, - properties).getName()); - } - }) - .toArray(String[]::new); - consumerDestination = new RabbitConsumerDestination( - StringUtils.arrayToCommaDelimitedString(provisionedDestinations), - null, group, name); - } - return consumerDestination; - } - - private ConsumerDestination doProvisionConsumerDestination(String name, String group, - ExtendedConsumerProperties properties) { - - boolean anonymous = !StringUtils.hasText(group); - String anonymousGroup = null; - if (anonymous) { - anonymousGroup = new Base64UrlNamingStrategy( - properties.getExtension().getAnonymousGroupPrefix() == null - ? "" - : properties.getExtension().getAnonymousGroupPrefix()).generateName(); - } - String baseQueueName; - if (properties.getExtension().isQueueNameGroupOnly()) { - baseQueueName = anonymous ? anonymousGroup : group; - } - else { - baseQueueName = groupedName(name, anonymous ? anonymousGroup : group); - } - if (this.logger.isInfoEnabled()) { - this.logger.info("declaring queue for inbound: " + baseQueueName - + ", bound to: " + name); - } - String prefix = properties.getExtension().getPrefix(); - String exchangeName = applyPrefix(prefix, name); - ContainerType containerType = properties.getExtension().getContainerType(); - boolean superStream = containerType.equals(ContainerType.STREAM) && properties.getExtension().isSuperStream(); - Exchange exchange = buildExchange(properties.getExtension(), exchangeName, null, null); - if (!superStream && properties.getExtension().isDeclareExchange()) { - declareExchange(exchangeName, anonymous ? anonymousGroup : group, exchange); - } - String queueName = applyPrefix(prefix, baseQueueName); - boolean partitioned = !anonymous && properties.isPartitioned(); - boolean durable = !anonymous && properties.getExtension().isDurableSubscription(); - Queue queue; - if (anonymous) { - String anonQueueName = queueName; - queue = new AnonymousQueue((org.springframework.amqp.core.NamingStrategy) () -> anonQueueName, - queueArgs(queueName, properties.getExtension(), false)); - queueName = queue.getName(); - } - else { - if (partitioned) { - String partitionSuffix = "-" + properties.getInstanceIndex(); - queueName += partitionSuffix; - } - if (durable) { - queue = new Queue(queueName, true, false, false, - queueArgs(queueName, properties.getExtension(), false)); - } - else { - queue = new Queue(queueName, false, false, true, - queueArgs(queueName, properties.getExtension(), false)); - } - } - Binding binding = null; - if (properties.getExtension().isBindQueue()) { - if (superStream) { - provisionSuperStream(properties, name); - } - else { - if (containerType.equals(ContainerType.STREAM)) { - queue.getArguments().put("x-queue-type", "stream"); - } - declareQueue(queueName, queue); - String[] routingKeys = bindingRoutingKeys(properties.getExtension()); - if (ObjectUtils.isEmpty(routingKeys)) { - binding = declareConsumerBindings(name, null, properties, exchange, partitioned, queue); - } - else { - for (String routingKey : routingKeys) { - binding = declareConsumerBindings(name, routingKey, properties, exchange, partitioned, queue); - } - } - } - } - if (durable && !superStream) { - autoBindDLQ(applyPrefix(properties.getExtension().getPrefix(), baseQueueName), - queueName, group, properties.getExtension()); - } - if (superStream) { - queueName = name; // group is used in the consumer for super streams so not part of the name. - } - return new RabbitConsumerDestination(queueName, binding, anonymous ? baseQueueName : group, name); - } - - private void provisionSuperStream(ExtendedConsumerProperties properties, - String name) { - - String routingKey = properties.getExtension().getBindingRoutingKey(); - String rk = routingKey == null ? name : routingKey; - SuperStream ss = new SuperStream(name, properties.getInstanceCount() * properties.getConcurrency(), - (q, i) -> IntStream.range(0, i) - .mapToObj(j -> rk + "-" + j) - .collect(Collectors.toList())); - synchronized (this.autoDeclareContext) { - if (!this.autoDeclareContext.containsBean(name + ".superStream")) { - this.autoDeclareContext.getBeanFactory().registerSingleton(name + ".superStream", ss); - } - } - try { - ss.getDeclarables().forEach(dec -> { - if (dec instanceof Exchange exch) { - this.rabbitAdmin.declareExchange(exch); - } - else if (dec instanceof Queue queue) { - this.rabbitAdmin.declareQueue(queue); - } - else if (dec instanceof Binding binding) { - this.rabbitAdmin.declareBinding(binding); - } - }); - } - catch (AmqpConnectException e) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Declaration of super stream: " + name - + " deferred - connection not available"); - } - } - - } - - /** - * Construct a name comprised of the name and group. - * @param name the name. - * @param group the group. - * @return the constructed name. - */ - protected final String groupedName(String name, String group) { - return name + GROUP_INDEX_DELIMITER - + (StringUtils.hasText(group) ? group : "default"); - } - - private Binding declareConsumerBindings(String name, String routingKey, - ExtendedConsumerProperties properties, - Exchange exchange, boolean partitioned, Queue queue) { - - if (partitioned) { - return partitionedBinding(name, exchange, queue, routingKey, properties.getExtension(), - properties.getInstanceIndex()); - } - else { - return notPartitionedBinding(exchange, queue, routingKey, properties.getExtension()); - } - } - - private Binding partitionedBinding(String destination, Exchange exchange, Queue queue, String rk, - RabbitCommonProperties extendedProperties, int index) { - - String bindingKey = rk; - if (bindingKey == null) { - bindingKey = destination; - } - bindingKey += "-" + index; - Map arguments = new HashMap<>(); - arguments.putAll(extendedProperties.getQueueBindingArguments()); - if (exchange instanceof TopicExchange topicExchange) { - Binding binding = BindingBuilder.bind(queue).to(topicExchange) - .with(bindingKey); - declareBinding(queue.getName(), binding); - return binding; - } - else if (exchange instanceof DirectExchange directExchange) { - Binding binding = BindingBuilder.bind(queue).to(directExchange) - .with(bindingKey); - declareBinding(queue.getName(), binding); - return binding; - } - else if (exchange instanceof FanoutExchange) { - throw new ProvisioningException( - "A fanout exchange is not appropriate for partitioned apps"); - } - else if (exchange instanceof HeadersExchange) { - Binding binding = new Binding(queue.getName(), DestinationType.QUEUE, exchange.getName(), "", arguments); - declareBinding(queue.getName(), binding); - return binding; - } - else { - throw new ProvisioningException( - "Cannot bind to a " + exchange.getType() + " exchange"); - } - } - - private Binding notPartitionedBinding(Exchange exchange, Queue queue, String rk, - RabbitCommonProperties extendedProperties) { - - String routingKey = rk; - if (routingKey == null) { - routingKey = "#"; - } - Map arguments = new HashMap<>(extendedProperties.getQueueBindingArguments()); - return createBinding(exchange, queue, routingKey, arguments, queue.getName()); - } - - private Binding createBinding(Exchange exchange, Queue queue, String routingKey, - @Nullable Map arguments, String beanName) { - - if (exchange instanceof TopicExchange) { - Binding binding = BindingBuilder.bind(queue).to((TopicExchange) exchange) - .with(routingKey); - declareBinding(beanName, binding); - return binding; - } - else if (exchange instanceof DirectExchange) { - Binding binding = BindingBuilder.bind(queue).to((DirectExchange) exchange) - .with(routingKey); - declareBinding(beanName, binding); - return binding; - } - else if (exchange instanceof FanoutExchange) { - Binding binding = BindingBuilder.bind(queue).to((FanoutExchange) exchange); - declareBinding(beanName, binding); - return binding; - } - else if (exchange instanceof HeadersExchange) { - Binding binding = new Binding(beanName, DestinationType.QUEUE, exchange.getName(), "", arguments); - declareBinding(queue.getName(), binding); - return binding; - } - else { - throw new ProvisioningException( - "Cannot bind to a " + exchange.getType() + " exchange"); - } - } - - private String[] bindingRoutingKeys(RabbitCommonProperties extendedProperties) { - /* - * When the delimiter is null, we get a String[1] containing the original. - */ - return StringUtils.delimitedListToStringArray(extendedProperties.getBindingRoutingKey(), - extendedProperties.getBindingRoutingKeyDelimiter()); - } - - /** - * If so requested, declare the DLX/DLQ and bind it. The DLQ is bound to the DLX with - * a routing key of the original queue name because we use default exchange routing by - * queue name for the original message. - * @param baseQueueName The base name for the queue (including the binder prefix, if - * any). - * @param routingKey The routing key for the queue. - * @param group The consumer group. - * @param properties the properties. - */ - private void autoBindDLQ(final String baseQueueName, String routingKey, String group, - RabbitCommonProperties properties) { - boolean autoBindDlq = properties.isAutoBindDlq(); - if (this.logger.isDebugEnabled()) { - this.logger.debug("autoBindDLQ=" + autoBindDlq + " for: " + baseQueueName); - } - if (autoBindDlq) { - String dlqName; - if (properties.getDeadLetterQueueName() == null) { - dlqName = constructDLQName(baseQueueName); - } - else { - dlqName = properties.getDeadLetterQueueName(); - } - Queue dlq = new Queue(dlqName, true, false, false, - queueArgs(dlqName, properties, true)); - declareQueue(dlqName, dlq); - String dlxName = deadLetterExchangeName(properties); - if (properties.isDeclareDlx()) { - declareExchange(dlxName, group, - new ExchangeBuilder(dlxName, - properties.getDeadLetterExchangeType()).durable(true) - .build()); - } - Map arguments = new HashMap<>(properties.getDlqBindingArguments()); - Binding dlqBinding = new Binding(dlq.getName(), DestinationType.QUEUE, - dlxName, properties.getDeadLetterRoutingKey() == null ? routingKey - : properties.getDeadLetterRoutingKey(), - arguments); - declareBinding(dlqName, dlqBinding); - if (properties instanceof RabbitConsumerProperties rabbitConsumerProperties - && rabbitConsumerProperties.isRepublishToDlq()) { - /* - * Also bind with the base queue name when republishToDlq is used, which - * does not know about partitioning - */ - declareBinding(dlqName + ".2", new Binding(dlq.getName(), DestinationType.QUEUE, - dlxName, baseQueueName, arguments)); - } - } - } - - /** - * For binder implementations that support dead lettering, construct the name of the - * dead letter entity for the underlying pipe name. - * @param name the name. - * @return constructDLQName - */ - public static String constructDLQName(String name) { - return name + ".dlq"; - } - - private String deadLetterExchangeName(RabbitCommonProperties properties) { - if (properties.getDeadLetterExchange() == null) { - return properties.getPrefix() + RabbitCommonProperties.DEAD_LETTER_EXCHANGE; - } - else { - return properties.getDeadLetterExchange(); - } - } - - private void declareQueue(String beanName, Queue queueArg) { - Queue queue = queueArg; - for (DeclarableCustomizer customizer : this.customizers) { - queue = (Queue) customizer.apply(queue); - } - try { - this.rabbitAdmin.declareQueue(queue); - } - catch (AmqpConnectException e) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Declaration of queue: " + queue.getName() - + " deferred - connection not available"); - } - } - catch (RuntimeException e) { - if (this.notOurAdminException) { - this.notOurAdminException = false; - throw e; - } - if (this.logger.isDebugEnabled()) { - this.logger.debug( - "Declaration of queue: " + queue.getName() + " deferred", e); - } - } - addToAutoDeclareContext(beanName, queue); - } - - private Map queueArgs(String queueName, - RabbitCommonProperties properties, boolean isDlq) { - Map args = new HashMap<>(); - if (!isDlq) { - if (properties.isAutoBindDlq()) { - String dlx; - if (properties.getDeadLetterExchange() != null) { - dlx = properties.getDeadLetterExchange(); - } - else { - dlx = applyPrefix(properties.getPrefix(), "DLX"); - } - args.put("x-dead-letter-exchange", dlx); - String dlRk; - if (properties.getDeadLetterRoutingKey() != null) { - dlRk = properties.getDeadLetterRoutingKey(); - } - else { - dlRk = queueName; - } - args.put("x-dead-letter-routing-key", dlRk); - } - } - else { - if (properties.getDlqDeadLetterExchange() != null) { - args.put("x-dead-letter-exchange", properties.getDlqDeadLetterExchange()); - } - if (properties.getDlqDeadLetterRoutingKey() != null) { - args.put("x-dead-letter-routing-key", - properties.getDlqDeadLetterRoutingKey()); - } - } - additionalArgs(args, properties, isDlq); - return args; - } - - private void additionalArgs(Map args, RabbitCommonProperties properties, boolean isDlq) { - Integer expires = isDlq ? properties.getDlqExpires() : properties.getExpires(); - Integer maxLength = isDlq ? properties.getDlqMaxLength() - : properties.getMaxLength(); - Integer maxLengthBytes = isDlq ? properties.getDlqMaxLengthBytes() - : properties.getMaxLengthBytes(); - Integer maxPriority = isDlq ? properties.getDlqMaxPriority() - : properties.getMaxPriority(); - Integer ttl = isDlq ? properties.getDlqTtl() : properties.getTtl(); - boolean lazy = isDlq ? properties.isDlqLazy() : properties.isLazy(); - String overflow = isDlq ? properties.getDlqOverflowBehavior() - : properties.getOverflowBehavior(); - QuorumConfig quorum = isDlq ? properties.getDlqQuorum() : properties.getQuorum(); - boolean singleActive = isDlq ? properties.isDlqSingleActiveConsumer() : properties.isSingleActiveConsumer(); - if (expires != null) { - args.put("x-expires", expires); - } - if (maxLength != null) { - args.put("x-max-length", maxLength); - } - if (maxLengthBytes != null) { - args.put("x-max-length-bytes", maxLengthBytes); - } - if (maxPriority != null) { - args.put("x-max-priority", maxPriority); - } - if (ttl != null) { - args.put("x-message-ttl", ttl); - } - if (lazy) { - args.put("x-queue-mode", "lazy"); - } - if (StringUtils.hasText(overflow)) { - args.put("x-overflow", overflow); - } - if (quorum != null && quorum.isEnabled()) { - args.put("x-queue-type", "quorum"); - if (quorum.getDeliveryLimit() != null) { - args.put("x-delivery-limit", quorum.getDeliveryLimit()); - } - if (quorum.getInitialGroupSize() != null) { - args.put("x-quorum-initial-group-size", quorum.getInitialGroupSize()); - } - } - if (singleActive) { - args.put("x-single-active-consumer", true); - } - } - - public static String applyPrefix(String prefix, String name) { - return prefix + name; - } - - private Exchange buildExchange(RabbitCommonProperties properties, String exchangeName, - @Nullable AlternateExchange alternate, @Nullable String beanNameQualifier) { - - try { - ExchangeBuilder builder = new ExchangeBuilder(exchangeName, - properties.getExchangeType()); - builder.durable(properties.isExchangeDurable()); - if (properties.isExchangeAutoDelete()) { - builder.autoDelete(); - } - if (properties.isDelayedExchange()) { - builder.delayed(); - } - if (alternate != null && !alternate.isExists()) { - builder.alternate(alternate.getName()); - configureAlternate(alternate, beanNameQualifier); - } - return builder.build(); - } - catch (Exception e) { - throw new ProvisioningException("Failed to create exchange object", e); - } - } - - private void configureAlternate(AlternateExchange alternate, String beanNameQualifier) { - Exchange exchange = customizeAndDeclare(new ExchangeBuilder(alternate.getName(), alternate.getType()) - .durable(true) - .build()); - addToAutoDeclareContext(alternate.getName() + "." + beanNameQualifier + ".exchange", exchange); - AlternateExchange.Binding binding = alternate.getBinding(); - if (binding != null) { - Queue queue = new Queue(binding.getQueue()); - String beanName = alternate.getName() + "." + binding.getQueue() + "." + beanNameQualifier; - declareQueue(beanName, queue); - Binding toBind = createBinding(exchange, queue, binding.getRoutingKey(), null, beanName); - } - } - - private void declareExchange(final String rootName, String group, final Exchange exchangeArg) { - Exchange exchange = customizeAndDeclare(exchangeArg); - addToAutoDeclareContext(rootName + "." + group + ".exchange", exchange); - } - - private Exchange customizeAndDeclare(Exchange exchange) { - for (DeclarableCustomizer customizer : this.customizers) { - exchange = (Exchange) customizer.apply(exchange); - } - try { - this.rabbitAdmin.declareExchange(exchange); - } - catch (AmqpConnectException e) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Declaration of exchange: " + exchange.getName() - + " deferred - connection not available"); - } - } - catch (RuntimeException e) { - if (this.notOurAdminException) { - this.notOurAdminException = false; - throw e; - } - if (this.logger.isDebugEnabled()) { - this.logger.debug( - "Declaration of exchange: " + exchange.getName() + " deferred", - e); - } - } - return exchange; - } - - private void addToAutoDeclareContext(String name, Declarable bean) { - synchronized (this.autoDeclareContext) { - if (!this.autoDeclareContext.containsBean(name)) { - this.autoDeclareContext.getBeanFactory().registerSingleton(name, new Declarables(bean)); - } - else { - this.autoDeclareContext.getBean(name, Declarables.class).getDeclarables().add(bean); - } - } - } - - private void declareBinding(String rootName, org.springframework.amqp.core.Binding bindingArg) { - Binding binding = bindingArg; - for (DeclarableCustomizer customizer : this.customizers) { - binding = (Binding) customizer.apply(binding); - } - try { - this.rabbitAdmin.declareBinding(binding); - } - catch (AmqpConnectException e) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Declaration of binding: " + rootName - + ".binding deferred - connection not available"); - } - } - catch (RuntimeException e) { - if (this.notOurAdminException) { - this.notOurAdminException = false; - throw e; - } - if (this.logger.isDebugEnabled()) { - this.logger.debug( - "Declaration of binding: " + rootName + ".binding deferred", e); - } - } - addToAutoDeclareContext(rootName + ".binding", binding); - } - - public void cleanAutoDeclareContext(ConsumerDestination destination, - ExtendedConsumerProperties consumerProperties) { - - synchronized (this.autoDeclareContext) { - Stream.of(StringUtils.tokenizeToStringArray(destination.getName(), ",", true, - true)).forEach(name -> { - String group = null; - String bindingName = null; - if (destination instanceof RabbitConsumerDestination rabbitConsumerDestination) { - group = rabbitConsumerDestination.getGroup(); - bindingName = rabbitConsumerDestination.getBindingName(); - } - RabbitConsumerProperties properties = consumerProperties.getExtension(); - String toRemove = properties.isQueueNameGroupOnly() ? bindingName + "." + group : name.trim(); - boolean partitioned = consumerProperties.isPartitioned(); - if (partitioned) { - toRemove = removePartitionPart(toRemove); - } - removeSingleton(toRemove + ".exchange"); - removeQueueAndBindingBeans(properties, name.trim(), "", group, partitioned); - }); - } - } - - public void cleanAutoDeclareContext(ProducerDestination dest, - ExtendedProducerProperties properties) { - - synchronized (this.autoDeclareContext) { - if (dest instanceof RabbitProducerDestination rabbitProducerDestination) { - String qual = rabbitProducerDestination.getBeanNameQualifier(); - removeSingleton(dest.getName() + "." + qual + ".exchange"); - String[] requiredGroups = properties.getRequiredGroups(); - if (!ObjectUtils.isEmpty(requiredGroups)) { - for (String group : requiredGroups) { - if (properties.isPartitioned()) { - for (int i = 0; i < properties.getPartitionCount(); i++) { - removeQueueAndBindingBeans(properties.getExtension(), - properties.getExtension().isQueueNameGroupOnly() ? "" : dest.getName(), - group + "-" + i, group, true); - } - } - else { - removeQueueAndBindingBeans(properties.getExtension(), dest.getName() + "." + group, "", - group, false); - } - } - } - AlternateExchange alternate = properties.getExtension().getAlternateExchange(); - if (alternate != null) { - removeSingleton(alternate.getName() + "." + qual + ".exchange"); - RabbitProducerProperties.AlternateExchange.Binding binding = alternate.getBinding(); - if (binding != null) { - removeSingleton(alternate.getName() + "." + binding.getQueue() + "." + qual); - removeSingleton(alternate.getName() + "." + binding.getQueue() + "." + qual + ".binding"); - } - } - } - } - } - - private void removeQueueAndBindingBeans(RabbitCommonProperties properties, String name, String suffix, - String group, boolean partitioned) { - - boolean suffixPresent = StringUtils.hasText(suffix); - String withSuffix = name + (suffixPresent ? ("." + suffix) : ""); - String nameDotOptional = name; - if (!StringUtils.hasText(name)) { - withSuffix = suffix; - } - else { - nameDotOptional = name + "."; - } - removeSingleton(withSuffix + ".binding"); - removeSingleton(withSuffix); - String dlq = (suffixPresent ? nameDotOptional + group : withSuffix) + ".dlq"; // only one DLQ when partitioned - if (StringUtils.hasText(properties.getDeadLetterQueueName())) { - dlq = properties.getDeadLetterQueueName(); - } - else if (partitioned) { - String removedPart = removePartitionPart(dlq); - if (!removedPart.endsWith(".dlq")) { - dlq = removedPart + ".dlq"; - } - } - removeSingleton(dlq + ".binding"); - removeSingleton(dlq + ".2.binding"); - removeSingleton(dlq); - removeSingleton(deadLetterExchangeName(properties) + "." + group + ".exchange"); - } - - private String removePartitionPart(String toRemove) { - int finalHyphen = toRemove.lastIndexOf("-"); - if (finalHyphen > 0) { - return toRemove.substring(0, finalHyphen); - } - return toRemove; - } - - private void removeSingleton(String name) { - if (this.autoDeclareContext.containsBean(name)) { - ConfigurableListableBeanFactory beanFactory = this.autoDeclareContext - .getBeanFactory(); - if (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory) { - defaultListableBeanFactory.destroySingleton(name); - } - } - } - - @Override - public void onApplicationEvent(DeclarationExceptionEvent event) { - this.notOurAdminException = true; // our admin doesn't have an event publisher - } - - private static final class RabbitProducerDestination implements ProducerDestination { - - private final Exchange exchange; - - private final Binding binding; - - private final String beanNameQualifier; - - RabbitProducerDestination(Exchange exchange, Binding binding, String beanNameQualifier) { - Assert.notNull(exchange, "exchange must not be null"); - this.exchange = exchange; - this.binding = binding; - this.beanNameQualifier = beanNameQualifier; - } - - @Override - public String getName() { - return this.exchange.getName(); - } - - @Override - public String getNameForPartition(int partition) { - return this.exchange.getName(); - } - - @Nullable - String getBeanNameQualifier() { - return this.beanNameQualifier; - } - - @Override - public String toString() { - return "RabbitProducerDestination{" + "exchange=" + this.exchange + ", binding=" - + this.binding + '}'; - } - - } - - private static final class RabbitConsumerDestination implements ConsumerDestination { - - private final String queue; - - private final Binding binding; - - private final String group; - - private final String bindingName; - - RabbitConsumerDestination(String queue, Binding binding, String group, String bindingName) { - Assert.notNull(queue, "queue must not be null"); - this.queue = queue; - this.binding = binding; - this.group = group; - this.bindingName = bindingName; - } - - @Override - public String getName() { - return this.queue; - } - - String getGroup() { - return this.group; - } - - String getBindingName() { - return this.bindingName; - } - - @Override - public String toString() { - return "RabbitConsumerDestination{" + "queue=" + this.queue + ", binding=" - + this.binding + ", group=" + this.group + ", bindingName=" + this.bindingName + '}'; - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java deleted file mode 100644 index 3c00dcadc..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-core/src/test/java/org/springframework/cloud/stream/binder/rabbit/provisioning/RabbitExchangeQueueProvisionerTests.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.rabbit.provisioning; - -import java.io.IOException; -import java.util.Map; -import java.util.Set; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.core.Declarables; -import org.springframework.amqp.core.DirectExchange; -import org.springframework.amqp.core.Exchange; -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.core.TopicExchange; -import org.springframework.amqp.rabbit.connection.Connection; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.AlternateExchange; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.AlternateExchange.Binding; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.context.ApplicationContext; -import org.springframework.expression.common.LiteralExpression; - -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.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; - -/** - * @author Gary Russell - * @since 3.2.3 - * - */ -public class RabbitExchangeQueueProvisionerTests { - - @Test - void consumerDeclarationsWithDlq() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitConsumerProperties props = new RabbitConsumerProperties(); - props.setAutoBindDlq(true); - ExtendedConsumerProperties properties = - new ExtendedConsumerProperties(props); - ConsumerDestination dest = provisioner.provisionConsumerDestination("foo", "group", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Set declarables = ctx.getBeansOfType(Declarables.class).keySet(); - assertThat(declarables).contains("foo.group.exchange", "foo.group", "foo.group.binding", "foo.group.dlq", - "DLX.group.exchange", "foo.group.dlq.binding", "foo.group.dlq.2.binding"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void consumerDeclarationsWithDlqQueueNameIsGroup() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitConsumerProperties props = new RabbitConsumerProperties(); - props.setAutoBindDlq(true); - props.setQueueNameGroupOnly(true); - ExtendedConsumerProperties properties = - new ExtendedConsumerProperties(props); - ConsumerDestination dest = provisioner.provisionConsumerDestination("fiz", "group", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Set declarables = ctx.getBeansOfType(Declarables.class).keySet(); - assertThat(declarables).contains("fiz.group.exchange", "group", "group.binding", "group.dlq", - "DLX.group.exchange", "group.dlq.binding", "group.dlq.2.binding"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void producerDeclarationsNoGroups() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitProducerProperties props = new RabbitProducerProperties(); - ExtendedProducerProperties properties = - new ExtendedProducerProperties(props); - ProducerDestination dest = provisioner.provisionProducerDestination("bar", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Set declarables = ctx.getBeansOfType(Declarables.class).keySet(); - String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); - assertThat(declarables).contains("bar." + qual + ".exchange"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void producerDeclarationsWithAlternateNoQueue() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitProducerProperties props = new RabbitProducerProperties(); - AlternateExchange alternate = new AlternateExchange(); - alternate.setName("altEx"); - props.setAlternateExchange(alternate); - ExtendedProducerProperties properties = - new ExtendedProducerProperties(props); - ProducerDestination dest = provisioner.provisionProducerDestination("withAlt", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Map declarables = ctx.getBeansOfType(Declarables.class); - assertThat(declarables).hasSize(2); - String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); - Declarables mainEx = declarables.get("withAlt." + qual + ".exchange"); - assertThat(mainEx).isNotNull(); - Exchange exch = (Exchange) mainEx.getDeclarables().iterator().next(); - assertThat(exch.getArguments().get("alternate-exchange")).isEqualTo("altEx"); - Declarables altEx = declarables.get("altEx." + qual + ".exchange"); - assertThat(altEx).isNotNull(); - exch = (Exchange) altEx.getDeclarables().iterator().next(); - assertThat(exch).isInstanceOf(TopicExchange.class); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void producerDeclarationsWithAlternateWithQueue() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitProducerProperties props = new RabbitProducerProperties(); - AlternateExchange alternate = new AlternateExchange(); - alternate.setName("altEx"); - alternate.setType(ExchangeTypes.DIRECT); - Binding binding = new Binding(); - binding.setQueue("altQ"); - binding.setRoutingKey("altRK"); - alternate.setBinding(binding); - props.setAlternateExchange(alternate); - ExtendedProducerProperties properties = - new ExtendedProducerProperties(props); - ProducerDestination dest = provisioner.provisionProducerDestination("withAlt", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Map declarables = ctx.getBeansOfType(Declarables.class); - assertThat(declarables).hasSize(4); - String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); - Declarables mainEx = declarables.get("withAlt." + qual + ".exchange"); - assertThat(mainEx).isNotNull(); - Exchange exch = (Exchange) mainEx.getDeclarables().iterator().next(); - assertThat(exch).isInstanceOf(TopicExchange.class); - assertThat(exch.getArguments().get("alternate-exchange")).isEqualTo("altEx"); - Declarables altEx = declarables.get("altEx." + qual + ".exchange"); - assertThat(altEx).isNotNull(); - exch = (Exchange) altEx.getDeclarables().iterator().next(); - assertThat(exch).isInstanceOf(DirectExchange.class); - Declarables queueDec = declarables.get("altEx.altQ." + qual); - assertThat(queueDec).isNotNull(); - Declarables bindingDec = declarables.get("altEx.altQ." + qual + ".binding"); - assertThat(bindingDec).isNotNull(); - org.springframework.amqp.core.Binding bdg = (org.springframework.amqp.core.Binding) bindingDec.getDeclarables() - .iterator().next(); - assertThat(bdg.getExchange()).isEqualTo("altEx"); - assertThat(bdg.getDestination()).isEqualTo("altQ"); - assertThat(bdg.getRoutingKey()).isEqualTo("altRK"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void producerDeclarationsWithGroupsAndDlq() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitProducerProperties props = new RabbitProducerProperties(); - props.setAutoBindDlq(true); - ExtendedProducerProperties properties = - new ExtendedProducerProperties(props); - properties.setRequiredGroups("group1", "group2"); - ProducerDestination dest = provisioner.provisionProducerDestination("baz", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Set declarables = ctx.getBeansOfType(Declarables.class).keySet(); - String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); - assertThat(declarables).contains("baz." + qual + ".exchange", "baz.group1", "baz.group1.binding", - "baz.group1.dlq", "DLX.group1.exchange", "baz.group1.dlq.binding", "baz.group2", "baz.group2.binding", - "baz.group2.dlq", "DLX.group2.exchange", "baz.group2.dlq.binding"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void producerDeclarationsWithGroupsAndDlqAndPartitions() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitProducerProperties props = new RabbitProducerProperties(); - props.setAutoBindDlq(true); - ExtendedProducerProperties properties = - new ExtendedProducerProperties(props); - properties.setRequiredGroups("group1", "group2"); - properties.setPartitionKeyExpression(new LiteralExpression("foo")); - properties.setPartitionCount(2); - ProducerDestination dest = provisioner.provisionProducerDestination("qux", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Set declarables = ctx.getBeansOfType(Declarables.class).keySet(); - String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); - assertThat(declarables).contains("qux." + qual + ".exchange", "qux.group1-0", "qux.group1-0.binding", - "qux.group1-1", "qux.group1-1.binding", "qux.group1.dlq", "DLX.group1.exchange", - "qux.group1.dlq.binding", "qux.group2-0", - "qux.group2-0.binding", "qux.group2-1", "qux.group2-1.binding", "qux.group2.dlq", "DLX.group2.exchange", - "qux.group2.dlq.binding"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - - @Test - void producerDeclarationsWithGroupsAndDlqAndPartitionsQueueNameIsGroup() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - willReturn(new DeclareOk("x", 0, 0)) - .given(channel).queueDeclare(any(), eq(Boolean.TRUE), eq(Boolean.FALSE), eq(Boolean.FALSE), any()); - given(conn.createChannel(anyBoolean())).willReturn(channel); - RabbitExchangeQueueProvisioner provisioner = new RabbitExchangeQueueProvisioner(cf); - - RabbitProducerProperties props = new RabbitProducerProperties(); - props.setAutoBindDlq(true); - props.setQueueNameGroupOnly(true); - ExtendedProducerProperties properties = - new ExtendedProducerProperties(props); - properties.setRequiredGroups("group1", "group2"); - properties.setPartitionKeyExpression(new LiteralExpression("foo")); - properties.setPartitionCount(2); - ProducerDestination dest = provisioner.provisionProducerDestination("qux", properties); - ApplicationContext ctx = - TestUtils.getPropertyValue(provisioner, "autoDeclareContext", ApplicationContext.class); - Set declarables = ctx.getBeansOfType(Declarables.class).keySet(); - String qual = TestUtils.getPropertyValue(dest, "beanNameQualifier", String.class); - assertThat(declarables).contains("qux." + qual + ".exchange", "group1-0", "group1-0.binding", "group1-1", - "group1-1.binding", "group1.dlq", "DLX.group1.exchange", "group1.dlq.binding", "group2-0", - "group2-0.binding", "group2-1", "group2-1.binding", "group2.dlq", "DLX.group2.exchange", - "group2.dlq.binding"); - provisioner.cleanAutoDeclareContext(dest, properties); - assertThat(ctx.getBeansOfType(Declarables.class)).isEmpty(); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/.jdk8 b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/pom.xml b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/pom.xml deleted file mode 100644 index f72d0e601..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/pom.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - 4.0.0 - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-parent - 4.1.0-SNAPSHOT - - spring-cloud-stream-binder-rabbit-test-support - Rabbit related test classes - - - org.springframework.boot - spring-boot-starter-logging - - - org.springframework.boot - spring-boot-starter-amqp - true - - - org.springframework.boot - spring-boot-starter-test - - - org.junit.jupiter - junit-jupiter - compile - - - - diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/AbstractExternalResourceTestSupport.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/AbstractExternalResourceTestSupport.java deleted file mode 100644 index 2566a5b58..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/AbstractExternalResourceTestSupport.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2013-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.cloud.stream.binder.test.junit.rabbit; - - - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; - -import org.springframework.util.Assert; - -import static org.junit.jupiter.api.Assertions.fail; - -/** - * Abstract base class for JUnit {@link Rule}s that detect the presence of some external - * resource. If the resource is indeed present, it will be available during the test - * lifecycle through {@link #getResource()}. If it is not, tests will either fail or be - * skipped, depending on the value of system property - * {@value #SCS_EXTERNAL_SERVERS_REQUIRED}. - * - * @param resource type - * @author Eric Bottard - * @author Gary Russell - */ -public abstract class AbstractExternalResourceTestSupport implements BeforeEachCallback { - - /** - * SCS external servers required environment variable. - */ - public static final String SCS_EXTERNAL_SERVERS_REQUIRED = "SCS_EXTERNAL_SERVERS_REQUIRED"; - - protected final Log logger = LogFactory.getLog(getClass()); - - protected R resource; - - private String resourceDescription; - - protected AbstractExternalResourceTestSupport(String resourceDescription) { - Assert.hasText(resourceDescription, "resourceDescription is required"); - this.resourceDescription = resourceDescription; - } - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - try { - obtainResource(); - } - catch (Exception e) { - maybeCleanup(); - -// failOrSkip(e); - fail(); - } - } - -// @Override -// public Statement apply(final Statement base, Description description) { -// try { -// obtainResource(); -// } -// catch (Exception e) { -// maybeCleanup(); -// -// return failOrSkip(e); -// } -// -// return new Statement() { -// -// @Override -// public void evaluate() throws Throwable { -// try { -// base.evaluate(); -// } -// finally { -// try { -// cleanupResource(); -// } -// catch (Exception ignored) { -// AbstractExternalResourceTestSupport.this.logger.warn( -// "Exception while trying to cleanup proper resource", -// ignored); -// } -// } -// } -// -// }; -// } - -// private Statement failOrSkip(final Exception e) { -// String serversRequired = System.getenv(SCS_EXTERNAL_SERVERS_REQUIRED); -// if ("true".equalsIgnoreCase(serversRequired)) { -// this.logger.error(this.resourceDescription + " IS REQUIRED BUT NOT AVAILABLE", -// e); -// fail(this.resourceDescription + " IS NOT AVAILABLE"); -// // Never reached, here to satisfy method signature -// return null; -// } -// else { -// this.logger.error( -// this.resourceDescription + " IS NOT AVAILABLE, SKIPPING TESTS", e); -// return new Statement() { -// -// @Override -// public void evaluate() throws Throwable { -// Assume.assumeTrue("Skipping test due to " -// + AbstractExternalResourceTestSupport.this.resourceDescription -// + " not being available " + e, false); -// } -// }; -// } -// } - - private void maybeCleanup() { - if (this.resource != null) { - try { - cleanupResource(); - } - catch (Exception ignored) { - this.logger.warn("Exception while trying to cleanup failed resource", - ignored); - } - } - } - - public R getResource() { - return this.resource; - } - - /** - * Perform cleanup of the {@link #resource} field, which is guaranteed to be non null. - * @throws Exception any exception thrown by this method will be logged and swallowed - */ - protected abstract void cleanupResource() throws Exception; - - /** - * Try to obtain and validate a resource. Implementors should either set the - * {@link #resource} field with a valid resource and return normally, or throw an - * exception. - * @throws Exception when resource couldn't be obtained - */ - protected abstract void obtainResource() throws Exception; - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/RabbitTestSupport.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/RabbitTestSupport.java deleted file mode 100644 index 989042ec1..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit-test-support/src/main/java/org/springframework/cloud/stream/binder/test/junit/rabbit/RabbitTestSupport.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder.test.junit.rabbit; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.net.ServerSocketFactory; -import javax.net.SocketFactory; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; - -/** - * JUnit {@link org.junit.Rule} that detects the fact that RabbitMQ is available on - * localhost. - * - * @author Mark Fisher - * @author Gary Russell - * @author Eric Bottard - */ -public class RabbitTestSupport - extends AbstractExternalResourceTestSupport { - - private final boolean management; - private final int ampqPort; - private final int managmentPort; - - public RabbitTestSupport() { - this(false); - } - - public RabbitTestSupport(boolean management) { - this(management, 5672, 15672); - } - - public RabbitTestSupport(boolean management, int amqpPort, int managementPort) { - super("RABBIT"); - this.management = management; - this.ampqPort = amqpPort; - this.managmentPort = managementPort; - - } - - @Override - protected void obtainResource() throws Exception { - resource = new CachingConnectionFactory("localhost", this.ampqPort); - resource.createConnection().close(); - if (management) { - Socket socket = SocketFactory.getDefault().createSocket("localhost", this.managmentPort); - socket.close(); - } - } - - @Override - protected void cleanupResource() throws Exception { - resource.destroy(); - } - - /** - * Test class to allow testing deferred entity declarations when RabbitMQ is down. - */ - public static class RabbitProxy { - - private static final Log LOGGER = LogFactory.getLog(RabbitProxy.class); - - private final int port; - - private final ExecutorService serverExec = Executors.newSingleThreadExecutor(); - - private final ExecutorService socketExec = Executors.newCachedThreadPool(); - - private volatile ServerSocket serverSocket; - - public RabbitProxy() throws IOException { - ServerSocket serverSocket = ServerSocketFactory.getDefault() - .createServerSocket(0); - this.port = serverSocket.getLocalPort(); - serverSocket.close(); - } - - public int getPort() { - return this.port; - } - - public void start() throws IOException { - start(5672); - } - - public void start(int amqpPort) throws IOException { - this.serverSocket = ServerSocketFactory.getDefault() - .createServerSocket(this.port, 10); - LOGGER.info("Proxy started"); - this.serverExec.execute(new Runnable() { - - @Override - public void run() { - try { - while (true) { - final Socket socket = serverSocket.accept(); - LOGGER.info("Accepted Connection"); - socketExec.execute(new Runnable() { - - @Override - public void run() { - try { - final Socket rabbitSocket = SocketFactory - .getDefault() - .createSocket("localhost", amqpPort); - socketExec.execute(new Runnable() { - - @Override - public void run() { - LOGGER.info("Running: " + rabbitSocket.getLocalPort()); - try { - InputStream is = rabbitSocket - .getInputStream(); - OutputStream os = socket - .getOutputStream(); - int c; - while ((c = is.read()) >= 0) { - os.write(c); - } - } - catch (IOException e) { - try { - socket.close(); - rabbitSocket.close(); - } - catch (IOException e1) { - } - } - } - }); - InputStream is = socket.getInputStream(); - OutputStream os = rabbitSocket.getOutputStream(); - int c; - while ((c = is.read()) >= 0) { - os.write(c); - } - } - catch (IOException e) { - try { - socket.close(); - } - catch (IOException e1) { - } - } - } - - }); - } - } - catch (IOException e) { - try { - serverSocket.close(); - } - catch (IOException e1) { - } - } - } - }); - } - - public void stop() throws IOException { - this.serverSocket.close(); - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/.jdk8 b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/pom.xml b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/pom.xml deleted file mode 100644 index 987574b6b..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/pom.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-binder-rabbit - jar - spring-cloud-stream-binder-rabbit - RabbitMQ binder implementation - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-parent - 4.1.0-SNAPSHOT - - - - - org.springframework.cloud - spring-cloud-function-deployer - ${spring-cloud-function.version} - true - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-core - ${project.version} - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.boot - spring-boot-autoconfigure - true - - - org.springframework.cloud - spring-cloud-connectors-core - true - 2.0.7.RELEASE - - - org.springframework.cloud - spring-cloud-spring-service-connector - true - provided - 2.0.7.RELEASE - - - org.springframework.boot - spring-boot-starter-amqp - - - org.springframework.amqp - spring-rabbit-stream - true - - - org.springframework.integration - spring-integration-amqp - - - org.springframework.integration - spring-integration-core - - - org.springframework.integration - spring-integration-jmx - - - org.springframework.integration - spring-integration-test - test - - - org.springframework.cloud - spring-cloud-stream-test-support - test - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-test-support - test - - - org.testcontainers - rabbitmq - 1.17.1 - test - - - - org.apache.httpcomponents - httpclient - 4.5.14 - - - diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/BatchCapableRejectAndDontRequeueRecoverer.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/BatchCapableRejectAndDontRequeueRecoverer.java deleted file mode 100644 index 588d1ee79..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/BatchCapableRejectAndDontRequeueRecoverer.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.rabbit; - -import java.util.List; -import java.util.function.Supplier; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.amqp.AmqpRejectAndDontRequeueException; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.rabbit.retry.MessageBatchRecoverer; -import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; -import org.springframework.util.Assert; - -public class BatchCapableRejectAndDontRequeueRecoverer implements MessageBatchRecoverer { - - protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR protected - - private final Supplier messageSupplier; - - public BatchCapableRejectAndDontRequeueRecoverer() { - this(() -> "Retry Policy Exhausted"); - } - - /** - * Construct an instance with the provided exception message supplier. - * - * @param messageSupplier the message supplier. - */ - public BatchCapableRejectAndDontRequeueRecoverer(Supplier messageSupplier) { - Assert.notNull(messageSupplier, "'messageSupplier' cannot be null"); - this.messageSupplier = messageSupplier; - } - - @Override - public void recover(List messages, Throwable cause) { - if (this.logger.isWarnEnabled()) { - this.logger.warn("Retries exhausted for message " + messages, cause); - } - throw new ListenerExecutionFailedException(this.messageSupplier.get(), - new AmqpRejectAndDontRequeueException(cause), messages.toArray(Message[]::new)); - } - - @Override - public void recover(Message message, Throwable cause) { - if (this.logger.isWarnEnabled()) { - this.logger.warn("Retries exhausted for message " + message, cause); - } - throw new ListenerExecutionFailedException(this.messageSupplier.get(), - new AmqpRejectAndDontRequeueException(cause), message); - } -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitExpressionEvaluatingInterceptor.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitExpressionEvaluatingInterceptor.java deleted file mode 100644 index 109038cca..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitExpressionEvaluatingInterceptor.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2017-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit; - -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.util.Assert; - -/** - * Interceptor to evaluate expressions for outbound messages before serialization. - * - * @author Gary Russell - * @author Oleg Zhurakousky - * @since 2.0 - * - */ -public class RabbitExpressionEvaluatingInterceptor implements ChannelInterceptor { - - /** - * Instance of ExpressionParser. - */ - public static final ExpressionParser PARSER = new SpelExpressionParser(); - - /** - * Default name for routing key header. - */ - public static final String ROUTING_KEY_HEADER = "scst_routingKey"; - - /** - * Default name for delay header. - */ - public static final String DELAY_HEADER = "scst_delay"; - - private final Expression routingKeyExpression; - - private final Expression delayExpression; - - private final EvaluationContext evaluationContext; - - /** - * Construct an instance with the provided expressions and evaluation context. At - * least one expression muse be non-null. - * @param routingKeyExpression the routing key expresssion. - * @param delayExpression the delay expression. - * @param evaluationContext the evaluation context. - */ - public RabbitExpressionEvaluatingInterceptor(Expression routingKeyExpression, - Expression delayExpression, EvaluationContext evaluationContext) { - Assert.isTrue(routingKeyExpression != null || delayExpression != null, - "At least one expression is required"); - Assert.notNull(evaluationContext, "the 'evaluationContext' cannot be null"); - if (routingKeyExpression != null) { - this.routingKeyExpression = routingKeyExpression; - } - else { - this.routingKeyExpression = null; - } - if (delayExpression != null) { - this.delayExpression = delayExpression; - } - else { - this.delayExpression = null; - } - this.evaluationContext = evaluationContext; - } - - @Override - public Message preSend(Message message, MessageChannel channel) { - MessageBuilder builder = MessageBuilder.fromMessage(message); - if (this.routingKeyExpression != null) { - builder.setHeader(ROUTING_KEY_HEADER, - this.routingKeyExpression.getValue(this.evaluationContext, message)); - } - if (this.delayExpression != null) { - builder.setHeader(DELAY_HEADER, - this.delayExpression.getValue(this.evaluationContext, message)); - } - return builder.build(); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java deleted file mode 100644 index 57d8955c4..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java +++ /dev/null @@ -1,1106 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Pattern; - -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Envelope; -import jakarta.validation.constraints.NotNull; - -import org.springframework.amqp.AmqpRejectAndDontRequeueException; -import org.springframework.amqp.ImmediateAcknowledgeAmqpException; -import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessagePostProcessor; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.rabbit.batch.BatchingStrategy; -import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; -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.connection.CorrelationData; -import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; -import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory; -import org.springframework.amqp.rabbit.connection.RabbitUtils; -import org.springframework.amqp.rabbit.core.BatchingRabbitTemplate; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; -import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; -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.support.AmqpHeaders; -import org.springframework.amqp.support.converter.AbstractMessageConverter; -import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.amqp.support.converter.SimpleMessageConverter; -import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; -import org.springframework.amqp.support.postprocessor.GZipPostProcessor; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties.Retry; -import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.HeaderMode; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitExtendedBindingProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.ProducerType; -import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.StaticMessageHeaderAccessor; -import org.springframework.integration.acks.AcknowledgmentCallback; -import org.springframework.integration.acks.AcknowledgmentCallback.Status; -import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; -import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter.BatchMode; -import org.springframework.integration.amqp.inbound.AmqpMessageSource; -import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; -import org.springframework.integration.amqp.support.AmqpHeaderMapper; -import org.springframework.integration.amqp.support.AmqpMessageHeaderErrorMessageStrategy; -import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.support.DefaultErrorMessageStrategy; -import org.springframework.integration.support.ErrorMessageStrategy; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.retry.RetryPolicy; -import org.springframework.retry.backoff.ExponentialBackOffPolicy; -import org.springframework.retry.policy.SimpleRetryPolicy; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by RabbitMQ. - * - * @author Mark Fisher - * @author Gary Russell - * @author Jennifer Hickey - * @author Gunnar Hillert - * @author Ilayaperumal Gopinathan - * @author David Turanski - * @author Marius Bogoevici - * @author Artem Bilan - * @author Soby Chacko - * @author Oleg Zhurakousky - * @author Christian Tzolov - * @author Byungjun You - */ -// @checkstyle:off -public class RabbitMessageChannelBinder extends - AbstractMessageChannelBinder, ExtendedProducerProperties, RabbitExchangeQueueProvisioner> - implements - ExtendedPropertiesBinder, - DisposableBean { - - private static final SimplePassthroughMessageConverter passThoughConverter = new SimplePassthroughMessageConverter(); - - private static final AmqpMessageHeaderErrorMessageStrategy errorMessageStrategy = new AmqpMessageHeaderErrorMessageStrategy(); - - private static final MessagePropertiesConverter inboundMessagePropertiesConverter = new DefaultMessagePropertiesConverter() { - @Override - public MessageProperties toMessageProperties(AMQP.BasicProperties source, - Envelope envelope, String charset) { - MessageProperties properties = super.toMessageProperties(source, envelope, - charset); - properties.setDeliveryMode(null); - return properties; - } - }; - - private static final Pattern interceptorNeededPattern = Pattern.compile("(payload|#root|#this)"); - - // @checkstyle:on - - private final RabbitProperties rabbitProperties; - - private boolean destroyConnectionFactory; - - private ConnectionFactory connectionFactory; - - private MessagePostProcessor decompressingPostProcessor = new DelegatingDecompressingPostProcessor(); - - private MessagePostProcessor compressingPostProcessor = new GZipPostProcessor(); - - private volatile String[] adminAddresses; - - private volatile String[] nodes; - - private volatile boolean clustered; - - private RabbitExtendedBindingProperties extendedBindingProperties = new RabbitExtendedBindingProperties(); - - public RabbitMessageChannelBinder(ConnectionFactory connectionFactory, - RabbitProperties rabbitProperties, - RabbitExchangeQueueProvisioner provisioningProvider) { - - this(connectionFactory, rabbitProperties, provisioningProvider, null, null); - } - - public RabbitMessageChannelBinder(ConnectionFactory connectionFactory, - RabbitProperties rabbitProperties, - RabbitExchangeQueueProvisioner provisioningProvider, - ListenerContainerCustomizer containerCustomizer) { - - this(connectionFactory, rabbitProperties, provisioningProvider, containerCustomizer, null); - } - - public RabbitMessageChannelBinder(ConnectionFactory connectionFactory, - RabbitProperties rabbitProperties, - RabbitExchangeQueueProvisioner provisioningProvider, - ListenerContainerCustomizer containerCustomizer, - MessageSourceCustomizer sourceCustomizer) { - - super(new String[0], provisioningProvider, containerCustomizer, sourceCustomizer); - Assert.notNull(connectionFactory, "connectionFactory must not be null"); - Assert.notNull(rabbitProperties, "rabbitProperties must not be null"); - this.connectionFactory = connectionFactory; - this.rabbitProperties = rabbitProperties; - } - - /** - * Set a {@link MessagePostProcessor} to decompress messages. Defaults to a {@link - * DelegatingDecompressingPostProcessor} with its default delegates. - * - * @param decompressingPostProcessor the post processor. - */ - public void setDecompressingPostProcessor( - MessagePostProcessor decompressingPostProcessor) { - this.decompressingPostProcessor = decompressingPostProcessor; - } - - /** - * Set a {@link org.springframework.amqp.core.MessagePostProcessor} to compress messages. Defaults to a {@link - * org.springframework.amqp.support.postprocessor.GZipPostProcessor}. - * - * @param compressingPostProcessor the post processor. - */ - public void setCompressingPostProcessor( - MessagePostProcessor compressingPostProcessor) { - this.compressingPostProcessor = compressingPostProcessor; - } - - public void setAdminAddresses(String[] adminAddresses) { - this.adminAddresses = Arrays.copyOf(adminAddresses, adminAddresses.length); - } - - public void setNodes(String[] nodes) { - this.nodes = Arrays.copyOf(nodes, nodes.length); - this.clustered = nodes.length > 1; - } - - public void setExtendedBindingProperties( - RabbitExtendedBindingProperties extendedBindingProperties) { - this.extendedBindingProperties = extendedBindingProperties; - } - - @Override - public void onInit() throws Exception { - super.onInit(); - if (this.clustered) { - String[] addresses = StringUtils.commaDelimitedListToStringArray( - this.rabbitProperties.getAddresses()); - - Assert.state( - addresses.length == this.adminAddresses.length - && addresses.length == this.nodes.length, - "'addresses', 'adminAddresses', and 'nodes' properties must have equal length"); - - this.connectionFactory = new LocalizedQueueConnectionFactory( - this.connectionFactory, addresses, this.adminAddresses, this.nodes, - this.rabbitProperties.getVirtualHost(), - this.rabbitProperties.getUsername(), - this.rabbitProperties.getPassword(), - this.rabbitProperties.getSsl().getEnabled(), - this.rabbitProperties.getSsl().getKeyStore(), - this.rabbitProperties.getSsl().getTrustStore(), - this.rabbitProperties.getSsl().getKeyStorePassword(), - this.rabbitProperties.getSsl().getTrustStorePassword()); - this.destroyConnectionFactory = true; - } - } - - @Override - public void destroy() throws Exception { - if (this.connectionFactory instanceof DisposableBean disposableConnectionFactory) { - if (this.destroyConnectionFactory) { - disposableConnectionFactory.destroy(); - } - } - } - - /** - * Get the underlying {@link ConnectionFactory} instance to allow manually altering the connection lifecycle. - */ - public ConnectionFactory getConnectionFactory() { - return this.connectionFactory; - } - - @Override - public RabbitConsumerProperties getExtendedConsumerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedConsumerProperties(channelName); - } - - @Override - public RabbitProducerProperties getExtendedProducerProperties(String channelName) { - return this.extendedBindingProperties.getExtendedProducerProperties(channelName); - } - - @Override - public String getDefaultsPrefix() { - return this.extendedBindingProperties.getDefaultsPrefix(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return this.extendedBindingProperties.getExtendedPropertiesEntryClass(); - } - - @Override - public String getBinderIdentity() { - return "rabbit-" + super.getBinderIdentity(); - } - - @Override - protected MessageHandler createProducerMessageHandler( - final ProducerDestination producerDestination, - ExtendedProducerProperties producerProperties, - MessageChannel errorChannel) { - Assert.state( - !HeaderMode.embeddedHeaders.equals(producerProperties.getHeaderMode()), - "the RabbitMQ binder does not support embedded headers since RabbitMQ supports headers natively"); - String prefix = producerProperties.getExtension().getPrefix(); - String exchangeName = producerDestination.getName(); - String destination = !StringUtils.hasText(prefix) ? exchangeName - : exchangeName.substring(prefix.length()); - RabbitProducerProperties extendedProperties = producerProperties.getExtension(); - final MessageHandler endpoint; - if (!ProducerType.AMQP.equals(producerProperties.getExtension().getProducerType())) { - endpoint = StreamUtils.createStreamMessageHandler(producerDestination, producerProperties, errorChannel, - destination, extendedProperties, getApplicationContext(), this::configureHeaderMapper); - } - else { - endpoint = amqpHandler(producerDestination, producerProperties, errorChannel, - destination, extendedProperties); - } - return endpoint; - } - - private AmqpOutboundEndpoint amqpHandler(final ProducerDestination producerDestination, - ExtendedProducerProperties producerProperties, MessageChannel errorChannel, - String destination, RabbitProducerProperties extendedProperties) { - final AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint( - buildRabbitTemplate(extendedProperties, - errorChannel != null || extendedProperties.isUseConfirmHeader())); - endpoint.setExchangeName(producerDestination.getName()); - boolean expressionInterceptorNeeded = expressionInterceptorNeeded( - extendedProperties); - Expression routingKeyExpression = extendedProperties.getRoutingKeyExpression(); - if (!producerProperties.isPartitioned()) { - if (routingKeyExpression == null) { - endpoint.setRoutingKey(destination); - } - else { - if (expressionInterceptorNeeded) { - endpoint.setRoutingKeyExpressionString("headers['" - + RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER - + "']"); - } - else { - endpoint.setRoutingKeyExpression(routingKeyExpression); - } - } - } - else { - if (routingKeyExpression == null) { - endpoint.setRoutingKeyExpression( - buildPartitionRoutingExpression(destination, false)); - } - else { - if (expressionInterceptorNeeded) { - endpoint.setRoutingKeyExpression( - buildPartitionRoutingExpression("headers['" - + RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER - + "']", true)); - } - else { - endpoint.setRoutingKeyExpression(buildPartitionRoutingExpression( - routingKeyExpression.getExpressionString(), true)); - } - } - } - if (extendedProperties.getDelayExpression() != null) { - if (expressionInterceptorNeeded) { - endpoint.setDelayExpressionString("headers['" - + RabbitExpressionEvaluatingInterceptor.DELAY_HEADER + "']"); - } - else { - endpoint.setDelayExpression(extendedProperties.getDelayExpression()); - } - } - endpoint.setHeaderMapper(configureHeaderMapper(extendedProperties)); - endpoint.setDefaultDeliveryMode(extendedProperties.getDeliveryMode()); - endpoint.setBeanFactory(this.getBeanFactory()); - if (errorChannel != null) { - checkConnectionFactoryIsErrorCapable(); - endpoint.setReturnChannel(errorChannel); - if (!extendedProperties.isUseConfirmHeader()) { - endpoint.setConfirmNackChannel(errorChannel); - String ackChannelBeanName = StringUtils - .hasText(extendedProperties.getConfirmAckChannel()) - ? extendedProperties.getConfirmAckChannel() - : IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME; - if (!ackChannelBeanName.equals(IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME) - && !getApplicationContext().containsBean(ackChannelBeanName)) { - GenericApplicationContext context = (GenericApplicationContext) getApplicationContext(); - context.registerBean(ackChannelBeanName, DirectChannel.class, - () -> new DirectChannel()); - } - endpoint.setConfirmAckChannelName(ackChannelBeanName); - endpoint.setConfirmCorrelationExpressionString("#root"); - } - else { - Assert.state(!StringUtils.hasText(extendedProperties.getConfirmAckChannel()), - "You cannot specify a 'confirmAckChannel' when 'useConfirmHeader' is true"); - } - endpoint.setErrorMessageStrategy(new DefaultErrorMessageStrategy()); - } - endpoint.setHeadersMappedLast(true); - return endpoint; - } - - private AmqpHeaderMapper configureHeaderMapper(RabbitProducerProperties extendedProperties) { - DefaultAmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.outboundMapper(); - List headerPatterns = new ArrayList<>(extendedProperties.getHeaderPatterns().length + 3); - if (!extendedProperties.isSuperStream()) { - // need to keep this header until later - headerPatterns.add("!" + BinderHeaders.PARTITION_HEADER); - } - headerPatterns.add("!" + IntegrationMessageHeaderAccessor.SOURCE_DATA); - headerPatterns.add("!" + IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); - headerPatterns.add("!rabbitmq_streamContext"); - headerPatterns.addAll(Arrays.asList(extendedProperties.getHeaderPatterns())); - mapper.setRequestHeaderNames( - headerPatterns.toArray(new String[headerPatterns.size()])); - return mapper; - } - - @Override - protected void postProcessOutputChannel(MessageChannel outputChannel, - ExtendedProducerProperties producerProperties) { - RabbitProducerProperties extendedProperties = producerProperties.getExtension(); - if (expressionInterceptorNeeded(extendedProperties)) { - ((AbstractMessageChannel) outputChannel).addInterceptor(0, - new RabbitExpressionEvaluatingInterceptor( - extendedProperties.getRoutingKeyExpression(), - extendedProperties.getDelayExpression(), - getEvaluationContext())); - } - } - - private boolean expressionInterceptorNeeded(RabbitProducerProperties extendedProperties) { - Expression rkExpression = extendedProperties.getRoutingKeyExpression(); - Expression delayExpression = extendedProperties.getDelayExpression(); - return (rkExpression != null && interceptorNeededPattern.matcher(rkExpression.getExpressionString()).find()) - || (delayExpression != null - && interceptorNeededPattern.matcher(delayExpression.getExpressionString()).find()); - } - - private void checkConnectionFactoryIsErrorCapable() { - if (!(this.connectionFactory instanceof CachingConnectionFactory)) { - logger.warn( - "Unknown connection factory type, cannot determine error capabilities: " - + this.connectionFactory.getClass()); - } - else { - CachingConnectionFactory ccf = (CachingConnectionFactory) this.connectionFactory; - if (!ccf.isPublisherConfirms() && !ccf.isPublisherReturns()) { - logger.warn( - "Producer error channel is enabled, but the connection factory is not configured for " - + "returns or confirms; the error channel will receive no messages"); - } - else if (!ccf.isPublisherConfirms()) { - logger.info( - "Producer error channel is enabled, but the connection factory is only configured to " - + "handle returned messages; negative acks will not be reported"); - } - else if (!ccf.isPublisherReturns()) { - logger.info( - "Producer error channel is enabled, but the connection factory is only configured to " - + "handle negatively acked messages; returned messages will not be reported"); - } - } - } - - private Expression buildPartitionRoutingExpression(String expressionRoot, - boolean rootIsExpression) { - String partitionRoutingExpression = rootIsExpression - ? expressionRoot + " + '-' + headers['" + BinderHeaders.PARTITION_HEADER - + "']" - : "'" + expressionRoot + "-' + headers['" + BinderHeaders.PARTITION_HEADER - + "']"; - return new SpelExpressionParser().parseExpression(partitionRoutingExpression); - } - - @Override - protected MessageProducer createConsumerEndpoint( - ConsumerDestination consumerDestination, String group, - ExtendedConsumerProperties properties) { - Assert.state(!HeaderMode.embeddedHeaders.equals(properties.getHeaderMode()), - "the RabbitMQ binder does not support embedded headers since RabbitMQ supports headers natively"); - String destination = consumerDestination.getName(); - RabbitConsumerProperties extension = properties.getExtension(); - MessageListenerContainer listenerContainer = createAndConfigureContainer(consumerDestination, group, - properties, destination, extension); - String[] queues = StringUtils.tokenizeToStringArray(destination, ",", true, true); - if (properties.getExtension().getContainerType() != ContainerType.STREAM - || !properties.getExtension().isSuperStream()) { - listenerContainer.setQueueNames(queues); - } - getContainerCustomizer().configure(listenerContainer, - consumerDestination.getName(), group); - listenerContainer.afterPropertiesSet(); - - AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(listenerContainer); - adapter.setBindSourceMessage(true); - adapter.setBeanFactory(this.getBeanFactory()); - adapter.setBeanName("inbound." + destination); - DefaultAmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.inboundMapper(); - mapper.setRequestHeaderNames(extension.getHeaderPatterns()); - adapter.setHeaderMapper(mapper); - ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure( - consumerDestination, group, properties); - if (properties.getMaxAttempts() > 1) { - adapter.setRetryTemplate(buildRetryTemplate(properties)); - adapter.setRecoveryCallback(errorInfrastructure.getRecoverer()); - } - else { - adapter.setErrorMessageStrategy(errorMessageStrategy); - adapter.setErrorChannel(errorInfrastructure.getErrorChannel()); - } - adapter.setMessageConverter(passThoughConverter); - ContainerType containerType = extension.getContainerType(); - if (properties.isBatchMode() && extension.isEnableBatching() - && ContainerType.SIMPLE.equals(containerType)) { - adapter.setBatchMode(BatchMode.EXTRACT_PAYLOADS_WITH_HEADERS); - } - if (containerType.equals(ContainerType.STREAM)) { - StreamUtils.configureAdapter(adapter); - } - return adapter; - } - - private MessageListenerContainer createAndConfigureContainer(ConsumerDestination consumerDestination, - String group, ExtendedConsumerProperties properties, String destination, - RabbitConsumerProperties extension) { - - if (extension.getContainerType().equals(ContainerType.STREAM)) { - return StreamUtils.createContainer(consumerDestination, group, properties, destination, - getApplicationContext()); - } - boolean directContainer = extension.getContainerType() - .equals(ContainerType.DIRECT); - AbstractMessageListenerContainer listenerContainer = directContainer - ? new DirectMessageListenerContainer(this.connectionFactory) - : new SimpleMessageListenerContainer(this.connectionFactory); - listenerContainer.setBeanName(consumerDestination.getName() + "." + group + ".container"); - listenerContainer - .setAcknowledgeMode(extension.getAcknowledgeMode()); - listenerContainer.setChannelTransacted(extension.isTransacted()); - listenerContainer - .setDefaultRequeueRejected(extension.isRequeueRejected()); - int concurrency = properties.getConcurrency(); - concurrency = concurrency > 0 ? concurrency : 1; - if (directContainer) { - setDMLCProperties(properties, - (DirectMessageListenerContainer) listenerContainer, concurrency); - } - else { - setSMLCProperties(properties, - (SimpleMessageListenerContainer) listenerContainer, concurrency); - } - listenerContainer.setPrefetchCount(extension.getPrefetch()); - listenerContainer - .setRecoveryInterval(extension.getRecoveryInterval()); - listenerContainer.setTaskExecutor( - new SimpleAsyncTaskExecutor(consumerDestination.getName() + "-")); - listenerContainer.setAfterReceivePostProcessors(this.decompressingPostProcessor); - listenerContainer.setMessagePropertiesConverter( - RabbitMessageChannelBinder.inboundMessagePropertiesConverter); - listenerContainer.setExclusive(extension.isExclusive()); - listenerContainer - .setMissingQueuesFatal(extension.getMissingQueuesFatal()); - if (extension.getFailedDeclarationRetryInterval() != null) { - listenerContainer.setFailedDeclarationRetryInterval( - extension.getFailedDeclarationRetryInterval()); - } - if (getApplicationEventPublisher() != null) { - listenerContainer - .setApplicationEventPublisher(getApplicationEventPublisher()); - } - else if (getApplicationContext() != null) { - listenerContainer.setApplicationEventPublisher(getApplicationContext()); - } - if (StringUtils.hasText(extension.getConsumerTagPrefix())) { - final AtomicInteger index = new AtomicInteger(); - listenerContainer.setConsumerTagStrategy( - q -> extension.getConsumerTagPrefix() + "#" - + index.getAndIncrement()); - } - return listenerContainer; - } - - private void setSMLCProperties( - ExtendedConsumerProperties properties, - SimpleMessageListenerContainer listenerContainer, int concurrency) { - - RabbitConsumerProperties extension = properties.getExtension(); - listenerContainer.setConcurrentConsumers(concurrency); - int maxConcurrency = extension.getMaxConcurrency(); - if (maxConcurrency > concurrency) { - listenerContainer.setMaxConcurrentConsumers(maxConcurrency); - } - listenerContainer.setDeBatchingEnabled(!properties.isBatchMode()); - listenerContainer.setBatchSize(extension.getBatchSize()); - if (extension.getQueueDeclarationRetries() != null) { - listenerContainer.setDeclarationRetries( - extension.getQueueDeclarationRetries()); - } - if (properties.isBatchMode() && extension.isEnableBatching()) { - listenerContainer.setConsumerBatchEnabled(true); - listenerContainer.setDeBatchingEnabled(true); - } - if (extension.getReceiveTimeout() != null) { - listenerContainer.setReceiveTimeout(extension.getReceiveTimeout()); - } - } - - private void setDMLCProperties( - ExtendedConsumerProperties properties, - DirectMessageListenerContainer listenerContainer, int concurrency) { - - listenerContainer.setConsumersPerQueue(concurrency); - if (properties.getExtension().getMaxConcurrency() > concurrency) { - this.logger - .warn("maxConcurrency is not supported by the direct container type"); - } - if (properties.getExtension().getBatchSize() > 1) { - this.logger.warn("batchSize is not supported by the direct container type"); - } - if (properties.getExtension().getQueueDeclarationRetries() != null) { - this.logger.warn( - "queueDeclarationRetries is not supported by the direct container type"); - } - } - - @Override - protected PolledConsumerResources createPolledConsumerResources(String name, - String group, ConsumerDestination destination, - ExtendedConsumerProperties consumerProperties) { - - Assert.isTrue(!consumerProperties.isMultiplex(), - "The Spring Integration polled MessageSource does not currently support muiltiple queues"); - AmqpMessageSource source = new AmqpMessageSource(this.connectionFactory, - destination.getName()); - source.setRawMessageHeader(true); - getMessageSourceCustomizer().configure(source, destination.getName(), group); - return new PolledConsumerResources(source, registerErrorInfrastructure( - destination, group, consumerProperties, true)); - } - - @Override - protected void postProcessPollableSource(DefaultPollableMessageSource bindingTarget) { - bindingTarget.setAttributesProvider((accessor, message) -> { - Object rawMessage = message.getHeaders() - .get(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE); - if (rawMessage != null) { - accessor.setAttribute( - AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE, - rawMessage); - } - }); - } - - @Override - protected ErrorMessageStrategy getErrorMessageStrategy() { - return errorMessageStrategy; - } - - @Override - protected MessageHandler getErrorMessageHandler(ConsumerDestination destination, - String group, - final ExtendedConsumerProperties properties) { - - if (properties.getExtension().isRepublishToDlq()) { - return new MessageHandler() { - - private static final long ACK_TIMEOUT = 10_000; - - private final RabbitTemplate template = new RabbitTemplate( - RabbitMessageChannelBinder.this.connectionFactory); - - private final ConfirmType confirmType; - - - - { - this.template.setUsePublisherConnection(true); - this.template.setChannelTransacted(properties.getExtension().isTransacted()); - this.template.setMandatory(RabbitMessageChannelBinder.this.connectionFactory.isPublisherReturns()); - if (RabbitMessageChannelBinder.this.connectionFactory.isSimplePublisherConfirms()) { - this.confirmType = ConfirmType.SIMPLE; - } - else if (RabbitMessageChannelBinder.this.connectionFactory.isPublisherConfirms()) { - this.confirmType = ConfirmType.CORRELATED; - } - else { - this.confirmType = ConfirmType.NONE; - } - } - - private final String exchange = deadLetterExchangeName(properties.getExtension()); - - private final String routingKey = properties.getExtension() - .getDeadLetterRoutingKey(); - - private final int frameMaxHeadroom = properties.getExtension() - .getFrameMaxHeadroom(); - - private int maxStackTraceLength = -1; - - private Boolean dlxPresent; - - @Override - public void handleMessage(org.springframework.messaging.Message message) throws MessagingException { - List amqpMessages = extractAmqpMessages(message, properties); - - if (!(message instanceof ErrorMessage)) { - logger.error("Expected an ErrorMessage, not a " - + message.getClass().toString() + " for: " + message); - } - else if (amqpMessages == null) { - logger.error("No raw message header in " + message); - } - else { - if (!checkDlx()) { - return; - } - Throwable cause = (Throwable) message.getPayload(); - if (!shouldRepublish(cause)) { - if (logger.isDebugEnabled()) { - logger.debug("Skipping republish of: " + message); - } - return; - } - - String stackTraceAsString = getStackTraceAsString(cause); - if (this.maxStackTraceLength < 0) { - int rabbitMaxStackTraceLength = RabbitUtils - .getMaxFrame(this.template.getConnectionFactory()); - if (rabbitMaxStackTraceLength > 0) { - // maxStackTraceLength -= this.frameMaxHeadroom; - this.maxStackTraceLength = rabbitMaxStackTraceLength - - this.frameMaxHeadroom; - } - } - if (this.maxStackTraceLength > 0 && stackTraceAsString - .length() > this.maxStackTraceLength) { - stackTraceAsString = stackTraceAsString.substring(0, - this.maxStackTraceLength); - logger.warn( - "Stack trace in republished message header truncated due to frame_max limitations; " - + "consider increasing frame_max on the broker or reduce the stack trace depth", - cause); - } - for (Message amqpMessage : amqpMessages) { - MessageProperties messageProperties = adjustMessagePropertiesHeader(cause, - stackTraceAsString, - amqpMessage); - doSend(this.exchange, - this.routingKey != null ? this.routingKey - : messageProperties.getConsumerQueue(), - amqpMessage); - } - if (properties.getExtension().getAcknowledgeMode().equals(AcknowledgeMode.MANUAL)) { - org.springframework.messaging.Message original = - ((ErrorMessage) message).getOriginalMessage(); - if (original != null) { - // If we are using manual acks, ack the original message. - try { - original.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class) - .basicAck(original.getHeaders() - .get(AmqpHeaders.DELIVERY_TAG, Long.class), false); - } - catch (IOException e) { - logger.debug("Failed to ack original message", e); - } - } - } - } - } - - @NotNull - private MessageProperties adjustMessagePropertiesHeader(Throwable cause, String stackTraceAsString, Message amqpMessage) { - MessageProperties messageProperties = amqpMessage - .getMessageProperties(); - Map headers = messageProperties.getHeaders(); - headers.put(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE, - stackTraceAsString); - headers.put(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE, - cause.getCause() != null ? cause.getCause().getMessage() - : cause.getMessage()); - headers.put(RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE, - messageProperties.getReceivedExchange()); - headers.put(RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY, - messageProperties.getReceivedRoutingKey()); - if (properties.getExtension().getRepublishDeliveyMode() != null) { - messageProperties.setDeliveryMode( - properties.getExtension().getRepublishDeliveyMode()); - } - return messageProperties; - } - - private void doSend(String exchange, String routingKey, Message amqpMessage) { - if (ConfirmType.SIMPLE.equals(this.confirmType)) { - this.template.invoke(temp -> { - temp.send(exchange, routingKey, amqpMessage); - if (!temp.waitForConfirms(ACK_TIMEOUT)) { - throw new AmqpRejectAndDontRequeueException("Negative ack for DLQ message received"); - } - return null; - }); - } - else if (ConfirmType.CORRELATED.equals(this.confirmType)) { - CorrelationData corr = new CorrelationData(); - this.template.send(exchange, routingKey, amqpMessage, corr); - try { - Confirm confirm = corr.getFuture().get(ACK_TIMEOUT, TimeUnit.MILLISECONDS); - if (!confirm.isAck()) { - throw new AmqpRejectAndDontRequeueException("Negative ack for DLQ message received"); - } - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new AmqpRejectAndDontRequeueException(e); - } - catch (ExecutionException e) { - throw new AmqpRejectAndDontRequeueException(e.getCause()); - } - catch (TimeoutException e) { - throw new AmqpRejectAndDontRequeueException(e); - } - if (corr.getReturned() != null) { - RabbitMessageChannelBinder.this.logger.error("DLQ message was returned: " + amqpMessage); - throw new AmqpRejectAndDontRequeueException("DLQ message was returned"); - } - } - else { - this.template.send(exchange, routingKey, amqpMessage); - } - } - - private boolean checkDlx() { - if (this.dlxPresent == null) { - if (properties.getExtension().isAutoBindDlq()) { - this.dlxPresent = Boolean.TRUE; - } - else { - this.dlxPresent = this.template.execute(channel -> { - String dlx = deadLetterExchangeName(properties.getExtension()); - try { - channel.exchangeDeclarePassive(dlx); - return Boolean.TRUE; - } - catch (IOException e) { - logger.warn("'republishToDlq' is true, but the '" - + dlx - + "' dead letter exchange is not present; disabling 'republishToDlq'"); - return Boolean.FALSE; - } - }); - } - } - return this.dlxPresent; - } - - /** - * Traverse the cause tree, stopping at AmqpRejectAndDontRequeueException - * or ImmediateAcknowledgeAmqpException. - * @param throwable the throwable. - * @return true if neither found or AmqpRejectAndDontRequeueException is - * found first. - */ - private boolean shouldRepublish(Throwable throwable) { - Throwable cause = throwable; - while (cause != null - && !(cause instanceof AmqpRejectAndDontRequeueException) - && !(cause instanceof ImmediateAcknowledgeAmqpException)) { - cause = cause.getCause(); - } - return !(cause instanceof ImmediateAcknowledgeAmqpException); - } - }; - } - else if (properties.getMaxAttempts() > 1) { - return new MessageHandler() { - private final BatchCapableRejectAndDontRequeueRecoverer recoverer = new BatchCapableRejectAndDontRequeueRecoverer(); - - @Override - public void handleMessage( - org.springframework.messaging.Message message) - throws MessagingException { - List amqpMessages = extractAmqpMessages(message, properties); - /* - * NOTE: The following IF and subsequent ELSE IF should never happen - * under normal interaction and it should always go to the last ELSE - * However, given that this is a handler subscribing to the public - * channel and that we can't control what type of Message may be sent - * to that channel (user decides to send a Message manually) the - * 'IF/ELSE IF' provides a safety net to handle any message properly. - */ - if (!(message instanceof ErrorMessage)) { - logger.error("Expected an ErrorMessage, not a " - + message.getClass().toString() + " for: " + message); - throw new ListenerExecutionFailedException( - "Unexpected error message " + message, - new AmqpRejectAndDontRequeueException(""), (Message[]) null); - } - else if (amqpMessages == null || amqpMessages.isEmpty()) { - logger.error("No raw message header in " + message); - throw new ListenerExecutionFailedException( - "Unexpected error message " + message, - new AmqpRejectAndDontRequeueException(""), amqpMessages.toArray(Message[]::new)); - } - else { - this.recoverer.recover(amqpMessages, - (Throwable) message.getPayload()); - } - } - - }; - } - else { - return super.getErrorMessageHandler(destination, group, properties); - } - } - - private List extractAmqpMessages(org.springframework.messaging.Message message, ExtendedConsumerProperties properties) { - - if (properties.isBatchMode() || properties.getExtension().isEnableBatching()) { - logger.debug("Batch mode enabled: Extract list instead of single message"); - return StaticMessageHeaderAccessor.getSourceData(message); - } - else { - Message amqpMessage = StaticMessageHeaderAccessor.getSourceData(message); - return List.of(amqpMessage); - } - } - - @Override - protected MessageHandler getPolledConsumerErrorMessageHandler( - ConsumerDestination destination, String group, - ExtendedConsumerProperties properties) { - MessageHandler handler = getErrorMessageHandler(destination, group, properties); - if (handler != null) { - return handler; - } - final MessageHandler superHandler = super.getErrorMessageHandler(destination, - group, properties); - return message -> { - Message amqpMessage = (Message) message.getHeaders() - .get(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE); - if (!(message instanceof ErrorMessage)) { - logger.error("Expected an ErrorMessage, not a " - + message.getClass().toString() + " for: " + message); - } - else if (amqpMessage == null) { - if (superHandler != null) { - superHandler.handleMessage(message); - } - } - else { - if (message.getPayload() instanceof MessagingException messagingException) { - AcknowledgmentCallback ack = StaticMessageHeaderAccessor - .getAcknowledgmentCallback(messagingException.getFailedMessage()); - if (ack != null) { - if (properties.getExtension().isRequeueRejected()) { - ack.acknowledge(Status.REQUEUE); - } - else { - ack.acknowledge(Status.REJECT); - } - } - } - } - }; - } - - // @Override - // protected String errorsBaseName(ConsumerDestination destination, String group, - // ExtendedConsumerProperties consumerProperties) { - // return destination.getName() + ".errors"; - // } - - private String deadLetterExchangeName(RabbitCommonProperties properties) { - if (properties.getDeadLetterExchange() == null) { - return applyPrefix(properties.getPrefix(), - RabbitCommonProperties.DEAD_LETTER_EXCHANGE); - } - else { - return properties.getDeadLetterExchange(); - } - } - - @Override - protected void afterUnbindConsumer(ConsumerDestination consumerDestination, String group, - ExtendedConsumerProperties consumerProperties) { - - this.provisioningProvider.cleanAutoDeclareContext(consumerDestination, - consumerProperties); - } - - @Override - protected void afterUnbindProducer(ProducerDestination destination, - ExtendedProducerProperties producerProperties) { - - this.provisioningProvider.cleanAutoDeclareContext(destination, producerProperties); - } - - private RabbitTemplate buildRabbitTemplate(RabbitProducerProperties properties, boolean mandatory) { - RabbitTemplate rabbitTemplate; - if (properties.isBatchingEnabled()) { - BatchingStrategy batchingStrategy = getBatchingStrategy(properties); - TaskScheduler taskScheduler = getApplicationContext() - .getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class); - rabbitTemplate = new BatchingRabbitTemplate(batchingStrategy, taskScheduler); - } - else { - rabbitTemplate = new RabbitTemplate(); - } - rabbitTemplate.setMessageConverter(passThoughConverter); - rabbitTemplate.setChannelTransacted(properties.isTransacted()); - rabbitTemplate.setConnectionFactory(this.connectionFactory); - rabbitTemplate.setUsePublisherConnection(true); - if (properties.isCompress()) { - rabbitTemplate.setBeforePublishPostProcessors(this.compressingPostProcessor); - } - rabbitTemplate.setMandatory(mandatory); // returned messages - if (rabbitProperties != null - && rabbitProperties.getTemplate().getRetry().isEnabled()) { - Retry retry = rabbitProperties.getTemplate().getRetry(); - RetryPolicy retryPolicy = new SimpleRetryPolicy(retry.getMaxAttempts()); - ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy(); - backOff.setInitialInterval(retry.getInitialInterval().toMillis()); - backOff.setMultiplier(retry.getMultiplier()); - backOff.setMaxInterval(retry.getMaxInterval().toMillis()); - RetryTemplate retryTemplate = new RetryTemplate(); - retryTemplate.setRetryPolicy(retryPolicy); - retryTemplate.setBackOffPolicy(backOff); - rabbitTemplate.setRetryTemplate(retryTemplate); - } - rabbitTemplate.afterPropertiesSet(); - return rabbitTemplate; - } - - private BatchingStrategy getBatchingStrategy(RabbitProducerProperties properties) { - BatchingStrategy batchingStrategy; - if (properties.getBatchingStrategyBeanName() != null) { - batchingStrategy = getApplicationContext() - .getBean(properties.getBatchingStrategyBeanName(), BatchingStrategy.class); - } - else { - batchingStrategy = new SimpleBatchingStrategy( - properties.getBatchSize(), - properties.getBatchBufferLimit(), - properties.getBatchTimeout() - ); - } - return batchingStrategy; - } - - private String getStackTraceAsString(Throwable cause) { - StringWriter stringWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stringWriter, true); - cause.printStackTrace(printWriter); - return stringWriter.getBuffer().toString(); - } - - private static final class SimplePassthroughMessageConverter - extends AbstractMessageConverter { - - private static final SimpleMessageConverter converter = new SimpleMessageConverter(); - - SimplePassthroughMessageConverter() { - super(); - } - - @Override - protected Message createMessage(Object object, - MessageProperties messageProperties) { - if (object instanceof byte[] bytes) { - return new Message(bytes, messageProperties); - } - else { - // just for safety (backwards compatibility) - return converter.toMessage(object, messageProperties); - } - } - - @Override - public Object fromMessage(Message message) throws MessageConversionException { - return message.getBody(); - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/StreamUtils.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/StreamUtils.java deleted file mode 100644 index 8dd501dc5..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/StreamUtils.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit; - -import java.util.Map; -import java.util.function.Function; - -import com.rabbitmq.stream.Environment; - -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.ProducerType; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.context.ApplicationContext; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; -import org.springframework.integration.amqp.outbound.RabbitStreamMessageHandler; -import org.springframework.integration.amqp.support.AmqpHeaderMapper; -import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.rabbit.stream.listener.ConsumerCustomizer; -import org.springframework.rabbit.stream.listener.StreamListenerContainer; -import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; -import org.springframework.rabbit.stream.support.StreamMessageProperties; -import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; - -/** - * Utilities for stream components. Used to prevent a hard runtime dependency on - * spring-rabbit-stream. - * - * @author Gary Russell - * @since 3.2 - * - */ -public final class StreamUtils { - - private StreamUtils() { - } - - /** - * Create a {@link StreamListenerContainer}. - * - * @param consumerDestination the destination. - * @param group the group. - * @param properties the properties. - * @param destination the destination. - * @param extension the properties extension. - * @param applicationContext the application context. - * @return the container. - */ - public static MessageListenerContainer createContainer(ConsumerDestination consumerDestination, String group, - ExtendedConsumerProperties properties, String destination, - ApplicationContext applicationContext) { - - RabbitConsumerProperties extension = properties.getExtension(); - - StreamListenerContainer container = new StreamListenerContainer(applicationContext.getBean(Environment.class)) { - - @Override - public synchronized void setConsumerCustomizer(ConsumerCustomizer consumerCustomizer) { - super.setConsumerCustomizer((id, builder) -> { - if (!properties.getExtension().isSuperStream()) { - builder.name(consumerDestination.getName() + "." + group); - } - consumerCustomizer.accept(id, builder); - }); - } - - - }; - container.setBeanName(consumerDestination.getName() + "." + group + ".container"); - String beanName = extension.getStreamStreamMessageConverterBeanName(); - if (beanName != null) { - container.setStreamConverter(applicationContext.getBean(beanName, StreamMessageConverter.class)); - } - if (properties.getExtension().isSuperStream()) { - container.superStream(consumerDestination.getName(), consumerDestination.getName() + "." + group, - properties.getConcurrency()); - } - return container; - } - - /** - * Configure the channel adapter for streams support. - * @param adapter the adapter. - */ - public static void configureAdapter(AmqpInboundChannelAdapter adapter) { - adapter.setHeaderMapper(new AmqpHeaderMapper() { - - AmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.inboundMapper(); - - @Override - public Map toHeadersFromRequest(MessageProperties source) { - Map headers = this.mapper.toHeadersFromRequest(source); - headers.put("rabbitmq_streamContext", ((StreamMessageProperties) source).getContext()); - return headers; - } - - @Override - public Map toHeadersFromReply(MessageProperties source) { - return null; - } - - @Override - public void fromHeadersToRequest(MessageHeaders headers, MessageProperties target) { - } - - @Override - public void fromHeadersToReply(MessageHeaders headers, MessageProperties target) { - } - - }); - } - - /** - * Create a {@link RabbitStreamMessageHandler}. - * - * @param producerDestination the destination. - * @param producerProperties the properties. - * @param errorChannel the error channel - * @param destination the destination. - * @param extendedProperties the extended properties. - * @param abstractApplicationContext the application context. - * @param headerMapperFunction the header mapper function. - * @return the handler. - */ - public static MessageHandler createStreamMessageHandler(ProducerDestination producerDestination, - ExtendedProducerProperties producerProperties, MessageChannel errorChannel, - String destination, RabbitProducerProperties extendedProperties, - AbstractApplicationContext applicationContext, - Function headerMapperFunction) { - - RabbitStreamTemplate template = new RabbitStreamTemplate(applicationContext.getBean(Environment.class), - producerDestination.getName()); - if (extendedProperties.isSuperStream()) { - template.setSuperStreamRouting(message -> { - Object property = message.getApplicationProperties().getOrDefault(BinderHeaders.PARTITION_HEADER, "0"); - message.getApplicationProperties().remove(BinderHeaders.PARTITION_HEADER); - return "" + property; - }); - } - String beanName = extendedProperties.getStreamMessageConverterBeanName(); - if (beanName != null) { - template.setMessageConverter(applicationContext.getBean(beanName, MessageConverter.class)); - } - beanName = extendedProperties.getStreamStreamMessageConverterBeanName(); - if (beanName != null) { - template.setStreamConverter(applicationContext.getBean(beanName, StreamMessageConverter.class)); - } - RabbitStreamMessageHandler handler = new RabbitStreamMessageHandler(template); - if (errorChannel != null) { - handler.setSendFailureChannel(errorChannel); - } - beanName = extendedProperties.getConfirmAckChannel(); - if (beanName != null) { - handler.setSendSuccessChannelName(beanName); - } - handler.setHeaderMapper(headerMapperFunction.apply(extendedProperties)); - handler.setSync(ProducerType.STREAM_SYNC.equals(producerProperties.getExtension().getProducerType())); - return handler; - } - -} - diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ExtendedBindingHandlerMappingsProviderConfiguration.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ExtendedBindingHandlerMappingsProviderConfiguration.java deleted file mode 100644 index fd196153c..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ExtendedBindingHandlerMappingsProviderConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.config; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.cloud.stream.config.BindingHandlerAdvise.MappingsProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration for extended binding metadata. - * - * @author Oleg Zhurakousky - * - */ -@Configuration(proxyBeanMethods = false) -public class ExtendedBindingHandlerMappingsProviderConfiguration { - - @Bean - public MappingsProvider rabbitExtendedPropertiesDefaultMappingsProvider() { - return () -> { - Map mappings = new HashMap<>(); - mappings.put( - ConfigurationPropertyName.of("spring.cloud.stream.rabbit.bindings"), - ConfigurationPropertyName.of("spring.cloud.stream.rabbit.default")); - return mappings; - }; - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ObservationAutoConfiguration.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ObservationAutoConfiguration.java deleted file mode 100644 index 60729f028..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/ObservationAutoConfiguration.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.config; - -import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; -import org.springframework.messaging.MessageHandler; - -/** - * @author Oleg Zhurakousky - * @author Byungjun You - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnBean(org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration.class) -public class ObservationAutoConfiguration { - - @Bean - @Order(Ordered.HIGHEST_PRECEDENCE) - ListenerContainerCustomizer observedListenerContainerCustomizer( - ApplicationContext applicationContext) { - return (container, destinationName, group) -> { - if (container instanceof AbstractMessageListenerContainer abstractMessageListenerContainer) { - abstractMessageListenerContainer.setObservationEnabled(true); - abstractMessageListenerContainer.setApplicationContext(applicationContext); - } - }; - } - - @Bean - @Order(Ordered.HIGHEST_PRECEDENCE) - ProducerMessageHandlerCustomizer observedProducerMessageHandlerCustomizer( - ApplicationContext applicationContext) { - return (handler, destinationName) -> { - if (handler instanceof AmqpOutboundEndpoint amqpOutboundEndpoint) { - amqpOutboundEndpoint.getRabbitTemplate().setObservationEnabled(true); - amqpOutboundEndpoint.getRabbitTemplate().setApplicationContext(applicationContext); - } - }; - } -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitBinderConfiguration.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitBinderConfiguration.java deleted file mode 100644 index 9a6a92ed3..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitBinderConfiguration.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2015-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.config; - -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.RabbitTemplate; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.amqp.RabbitHealthIndicator; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.Cloud; -import org.springframework.cloud.CloudFactory; -import org.springframework.cloud.service.messaging.RabbitConnectionFactoryConfig; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; -import org.springframework.util.StringUtils; - -/** - * Bind to services, either locally or in a cloud environment. - * - * @author Mark Fisher - * @author Dave Syer - * @author Glenn Renfro - * @author David Turanski - * @author Eric Bottard - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Artem Bilan - * @author Gary Russell - * @author Chris Bono - * @author Soby Chacko - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(Binder.class) -@Import({ RabbitMessageChannelBinderConfiguration.class, - RabbitBinderConfiguration.RabbitHealthIndicatorConfiguration.class }) -public class RabbitBinderConfiguration { - - public String binderName() { - return "rabbit"; - } - - static void configureCachingConnectionFactory( - CachingConnectionFactory connectionFactory, - ConfigurableApplicationContext applicationContext, - RabbitProperties rabbitProperties) throws Exception { - - if (StringUtils.hasText(rabbitProperties.getAddresses())) { - connectionFactory.setAddresses(rabbitProperties.determineAddresses()); - } - - connectionFactory.setPublisherConfirmType(rabbitProperties.getPublisherConfirmType() == null ? ConfirmType.NONE : rabbitProperties.getPublisherConfirmType()); - connectionFactory.setPublisherReturns(rabbitProperties.isPublisherReturns()); - if (rabbitProperties.getCache().getChannel().getSize() != null) { - connectionFactory.setChannelCacheSize( - rabbitProperties.getCache().getChannel().getSize()); - } - if (rabbitProperties.getCache().getConnection().getMode() != null) { - connectionFactory - .setCacheMode(rabbitProperties.getCache().getConnection().getMode()); - } - if (rabbitProperties.getCache().getConnection().getSize() != null) { - connectionFactory.setConnectionCacheSize( - rabbitProperties.getCache().getConnection().getSize()); - } - if (rabbitProperties.getCache().getChannel().getCheckoutTimeout() != null) { - connectionFactory.setChannelCheckoutTimeout(rabbitProperties.getCache() - .getChannel().getCheckoutTimeout().toMillis()); - } - connectionFactory.setApplicationContext(applicationContext); - applicationContext.addApplicationListener(connectionFactory); - connectionFactory.afterPropertiesSet(); - } - - /** - * Configuration to be used when the cloud profile is set. - */ - @Configuration(proxyBeanMethods = false) - @Profile("cloud") - protected static class CloudProfile { - - /** - * Configuration to be used when the cloud profile is set, and Cloud Connectors - * are found on the classpath. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Cloud.class) - protected static class CloudConnectors { - - @Bean - @ConditionalOnMissingBean - public Cloud cloud() { - return new CloudFactory().getCloud(); - } - - /** - * Active only if {@code spring.cloud.stream.override-cloud-connectors} is not - * set to {@code true}. - */ - // @checkstyle:off - @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(value = "spring.cloud.stream.override-cloud-connectors", havingValue = "false", matchIfMissing = true) - // @checkstyle:on - // Required to parse Rabbit properties which are passed to the binder for - // clustering. We need to enable it here explicitly as the default Rabbit - // configuration is not triggered. - @EnableConfigurationProperties(RabbitProperties.class) - protected static class UseCloudConnectors { - - /** - * Creates a {@link ConnectionFactory} using the singleton service - * connector. - * @param cloud {@link Cloud} instance to be used for accessing services. - * @param connectorConfigObjectProvider the {@link ObjectProvider} for the - * {@link RabbitConnectionFactoryConfig}. - * @param applicationContext application context instance - * @param rabbitProperties rabbit properties - * @return the {@link ConnectionFactory} used by the binder. - * @throws Exception if configuration of connection factory fails - */ - @Bean - @Primary - ConnectionFactory rabbitConnectionFactory(Cloud cloud, - ObjectProvider connectorConfigObjectProvider, - ConfigurableApplicationContext applicationContext, - RabbitProperties rabbitProperties) throws Exception { - - ConnectionFactory connectionFactory = cloud - .getSingletonServiceConnector(ConnectionFactory.class, - connectorConfigObjectProvider.getIfUnique()); - - configureCachingConnectionFactory( - (CachingConnectionFactory) connectionFactory, - applicationContext, rabbitProperties); - - return connectionFactory; - } - - @Bean - @ConditionalOnMissingBean(RabbitTemplate.class) - RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { - return new RabbitTemplate(connectionFactory); - } - - } - - /** - * Configuration to be used if - * {@code spring.cloud.stream.override-cloud-connectors} is set to - * {@code true}. Defers to Spring Boot auto-configuration. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty("spring.cloud.stream.override-cloud-connectors") - @Import(RabbitConfiguration.class) - protected static class OverrideCloudConnectors { - - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingClass("org.springframework.cloud.Cloud") - @Import(RabbitConfiguration.class) - protected static class NoCloudConnectors { - - } - - } - - /** - * Configuration to be used when the cloud profile is not set. Defer to Spring Boot - * auto-configuration. - */ - @Configuration(proxyBeanMethods = false) - @Profile("!cloud") - @Import(RabbitConfiguration.class) - protected static class NoCloudProfile { - - } - - /** - * Configuration for Rabbit health indicator. - * - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") - @ConditionalOnEnabledHealthIndicator("binders") - public static class RabbitHealthIndicatorConfiguration { - - @Bean - public HealthIndicator binderHealthIndicator(RabbitTemplate rabbitTemplate) { - return new RabbitHealthIndicator(rabbitTemplate); - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitConfiguration.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitConfiguration.java deleted file mode 100644 index 1d9744677..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitConfiguration.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2015-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.config; - -import java.util.stream.Collectors; - -import com.rabbitmq.client.impl.CredentialsProvider; -import com.rabbitmq.client.impl.CredentialsRefreshService; - -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; -import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; -import org.springframework.amqp.rabbit.core.RabbitOperations; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.amqp.CachingConnectionFactoryConfigurer; -import org.springframework.boot.autoconfigure.amqp.ConnectionFactoryCustomizer; -import org.springframework.boot.autoconfigure.amqp.RabbitConnectionFactoryBeanConfigurer; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.boot.autoconfigure.amqp.RabbitRetryTemplateCustomizer; -import org.springframework.boot.autoconfigure.amqp.RabbitTemplateConfigurer; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ResourceLoader; - -/** - * Configuration for {@link RabbitTemplate} and {@link CachingConnectionFactory}. - * - * @author Chris Bono - * @since 3.2 - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(RabbitProperties.class) -public class RabbitConfiguration { - - @Bean - @ConditionalOnMissingBean - RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(RabbitProperties properties, - ResourceLoader resourceLoader, ObjectProvider credentialsProvider, - ObjectProvider credentialsRefreshService) { - RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, properties); - configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); - configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); - return configurer; - } - - @Bean - @ConditionalOnMissingBean - CachingConnectionFactoryConfigurer rabbitConnectionFactoryConfigurer(RabbitProperties rabbitProperties, - ObjectProvider connectionNameStrategy) { - CachingConnectionFactoryConfigurer configurer = new CachingConnectionFactoryConfigurer(rabbitProperties); - configurer.setConnectionNameStrategy(connectionNameStrategy.getIfUnique()); - return configurer; - } - - @Bean - @ConditionalOnMissingBean(ConnectionFactory.class) - CachingConnectionFactory rabbitConnectionFactory( - RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer, - CachingConnectionFactoryConfigurer rabbitCachingConnectionFactoryConfigurer, - ObjectProvider connectionFactoryCustomizers) throws Exception { - - RabbitConnectionFactoryBean connectionFactoryBean = new RabbitConnectionFactoryBean(); - rabbitConnectionFactoryBeanConfigurer.configure(connectionFactoryBean); - connectionFactoryBean.afterPropertiesSet(); - com.rabbitmq.client.ConnectionFactory connectionFactory = connectionFactoryBean.getObject(); - connectionFactoryCustomizers.orderedStream() - .forEach((customizer) -> customizer.customize(connectionFactory)); - - CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(connectionFactory); - rabbitCachingConnectionFactoryConfigurer.configure(cachingConnectionFactory); - - return cachingConnectionFactory; - } - - @Bean - @ConditionalOnMissingBean - public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties, - ObjectProvider messageConverter, - ObjectProvider retryTemplateCustomizers) { - RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer(properties); - configurer.setMessageConverter(messageConverter.getIfUnique()); - configurer.setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList())); - return configurer; - } - - @Bean - @ConditionalOnSingleCandidate(ConnectionFactory.class) - @ConditionalOnMissingBean(RabbitOperations.class) - public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) { - RabbitTemplate template = new RabbitTemplate(); - configurer.configure(template, connectionFactory); - return template; - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitMessageChannelBinderConfiguration.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitMessageChannelBinderConfiguration.java deleted file mode 100644 index 1ffb084f9..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/config/RabbitMessageChannelBinderConfiguration.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.config; - -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.amqp.core.DeclarableCustomizer; -import org.springframework.amqp.core.MessagePostProcessor; -import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; -import org.springframework.amqp.support.postprocessor.GZipPostProcessor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitBinderConfigurationProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitExtendedBindingProperties; -import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; -import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; -import org.springframework.integration.amqp.inbound.AmqpMessageSource; -import org.springframework.lang.Nullable; -import org.springframework.messaging.MessageHandler; -import org.springframework.util.CollectionUtils; - - -/** - * Configuration class for RabbitMQ message channel binder. - * - * @author David Turanski - * @author Vinicius Carvalho - * @author Artem Bilan - * @author Oleg Zhurakousky - * @author Gary Russell - * @author Ben Blinebury - * @author Byungjun You - */ -@Configuration(proxyBeanMethods = false) -@Import({ PropertyPlaceholderAutoConfiguration.class }) -@EnableConfigurationProperties({ RabbitBinderConfigurationProperties.class, - RabbitExtendedBindingProperties.class }) -public class RabbitMessageChannelBinderConfiguration { - - @Autowired - private ConnectionFactory rabbitConnectionFactory; - - @Autowired - private RabbitProperties rabbitProperties; - - @Autowired - private RabbitBinderConfigurationProperties rabbitBinderConfigurationProperties; - - @Autowired - private RabbitExtendedBindingProperties rabbitExtendedBindingProperties; - - @Bean - RabbitMessageChannelBinder rabbitMessageChannelBinder( - @Nullable List> listenerContainerCustomizers, - @Nullable MessageSourceCustomizer sourceCustomizer, - @Nullable List> producerMessageHandlerCustomizers, - @Nullable ConsumerEndpointCustomizer consumerCustomizer, - List declarableCustomizers, - @Nullable ConnectionNameStrategy connectionNameStrategy) { - - String connectionNamePrefix = this.rabbitBinderConfigurationProperties.getConnectionNamePrefix(); - if (this.rabbitConnectionFactory instanceof AbstractConnectionFactory connectionFactory - && connectionNamePrefix != null && connectionNameStrategy == null) { - final AtomicInteger nameIncrementer = new AtomicInteger(); - connectionFactory.setConnectionNameStrategy(f -> connectionNamePrefix - + "#" + nameIncrementer.getAndIncrement()); - } - - ListenerContainerCustomizer composedCistomizer = new ListenerContainerCustomizer<>() { - @Override - public void configure(MessageListenerContainer container, String destinationName, String group) { - if (!CollectionUtils.isEmpty(listenerContainerCustomizers)) { - for (ListenerContainerCustomizer customizer : listenerContainerCustomizers) { - customizer.configure(container, destinationName, group); - } - } - } - }; - ProducerMessageHandlerCustomizer producerMessageHandlerCustomizer = new ProducerMessageHandlerCustomizer<>() { - @Override - public void configure(MessageHandler handler, String destinationName) { - if (!CollectionUtils.isEmpty(producerMessageHandlerCustomizers)) { - for (ProducerMessageHandlerCustomizer customizer : producerMessageHandlerCustomizers) { - customizer.configure(handler, destinationName); - } - } - } - }; - - RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder( - this.rabbitConnectionFactory, this.rabbitProperties, - provisioningProvider(declarableCustomizers), composedCistomizer, sourceCustomizer); - binder.setAdminAddresses( - this.rabbitBinderConfigurationProperties.getAdminAddresses()); - binder.setCompressingPostProcessor(gZipPostProcessor()); - binder.setDecompressingPostProcessor(deCompressingPostProcessor()); - binder.setNodes(this.rabbitBinderConfigurationProperties.getNodes()); - binder.setExtendedBindingProperties(this.rabbitExtendedBindingProperties); - binder.setProducerMessageHandlerCustomizer(producerMessageHandlerCustomizer); - binder.setConsumerEndpointCustomizer(consumerCustomizer); - return binder; - } - - @Bean - MessagePostProcessor deCompressingPostProcessor() { - return new DelegatingDecompressingPostProcessor(); - } - - @Bean - MessagePostProcessor gZipPostProcessor() { - GZipPostProcessor gZipPostProcessor = new GZipPostProcessor(); - - if (this.rabbitBinderConfigurationProperties.getCompressionLevel() != null) { - gZipPostProcessor.setLevel(this.rabbitBinderConfigurationProperties.getCompressionLevel()); - } - - return gZipPostProcessor; - } - - @Bean - RabbitExchangeQueueProvisioner provisioningProvider(List customizers) { - return new RabbitExchangeQueueProvisioner(this.rabbitConnectionFactory, customizers); - } -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployer.java.todo b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployer.java.todo deleted file mode 100644 index f31c9a48a..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployer.java.todo +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.deployer; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - - -/** - * Main application class to trigger the deployment of Java Functions based - * user application with RabitMQ bindings. - *
- *
- * It is based on Spring Cloud Function - * deploy feature - * - *
- * All you need to do is to provide few additional properties to assist Spring Cloud Function deployer. - *
- * For example: - *
- *
- *
- * java -jar spring-cloud-stream-binder-rabbit-deployer/target/spring-cloud-stream-binder-rabbit-deployer-3.2.0-SNAPSHOT.jar\
- * 		 	--spring.cloud.function.location=/foo/bar/simplestjar-1.0.0.RELEASE.jar
- * 			--spring.cloud.function.function-class=function.example.UpperCaseFunction
- *
- * 
- * The only required property is 'spring.cloud.function.location'. Every other property is optional and is based on the style of user application. - * For more details on the supported styles of user application please refer to - * - * Deploying a Packaged Function section of Spring Cloud Function. - * - * @author Oleg Zhurakousky - * @since 3.1.2 - * - */ -@SpringBootApplication -public class RabbitDeployer { - - public static void main(String[] args) { - SpringApplication.run(RabbitDeployer.class, args); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployerEnvironmentPostProcessor.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployerEnvironmentPostProcessor.java deleted file mode 100644 index 91fead450..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/deployer/RabbitDeployerEnvironmentPostProcessor.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.deployer; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.env.ConfigurableEnvironment; - -/** - * - * @author Oleg Zhurakousky - * - * @since 3.2 - * - */ -class RabbitDeployerEnvironmentPostProcessor implements EnvironmentPostProcessor { - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, - SpringApplication application) { - if (!environment.containsProperty("spring.cloud.function.rsocket.enabled")) { - environment.getSystemProperties().putIfAbsent("spring.cloud.function.rsocket.enabled", false); - } - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.binders b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.binders deleted file mode 100644 index 78f94972b..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -rabbit:\ -org.springframework.cloud.stream.binder.rabbit.config.RabbitBinderConfiguration diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.factories b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 627208331..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor:\ -org.springframework.cloud.stream.binder.rabbit.deployer.RabbitDeployerEnvironmentPostProcessor diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 293619926..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.cloud.stream.binder.rabbit.config.ExtendedBindingHandlerMappingsProviderConfiguration -org.springframework.cloud.stream.binder.rabbit.config.ObservationAutoConfiguration diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/BatchCapableRejectAndDontRequeueRecovererTest.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/BatchCapableRejectAndDontRequeueRecovererTest.java deleted file mode 100644 index 0e700063d..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/BatchCapableRejectAndDontRequeueRecovererTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.rabbit; - -import java.util.List; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.AmqpRejectAndDontRequeueException; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.rabbit.retry.MessageBatchRecoverer; -import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; - -import static org.assertj.core.api.Assertions.assertThat; - -class BatchCapableRejectAndDontRequeueRecovererTest { - - @Test - void testBatchRecoverThrowExceptionCorrectly() { - MessageBatchRecoverer messageBatchRecoverer = new BatchCapableRejectAndDontRequeueRecoverer(); - - Message expectedMessage1 = new Message(new byte[] {}); - Message expectedMessage2 = new Message(new byte[] {}); - - List messages = List.of(expectedMessage1, expectedMessage2); - - Throwable expectedThrowable = new RuntimeException("test"); - - ListenerExecutionFailedException exception = Assertions.assertThrows(ListenerExecutionFailedException.class, - () -> messageBatchRecoverer.recover(messages, expectedThrowable)); - - assertThat(exception.getFailedMessages()).contains(expectedMessage1); - assertThat(exception.getFailedMessages()).contains(expectedMessage2); - assertThat(exception.getCause()).isInstanceOf(AmqpRejectAndDontRequeueException.class); - assertThat(expectedThrowable).isEqualTo(exception.getCause().getCause()); - } - - @Test - void testRecoverThrowExceptionCorrectly() { - MessageBatchRecoverer messageBatchRecoverer = new BatchCapableRejectAndDontRequeueRecoverer(); - - Message expectedMessage = new Message(new byte[] {}); - - Throwable expectedThrowable = new RuntimeException("test"); - - ListenerExecutionFailedException exception = Assertions.assertThrows(ListenerExecutionFailedException.class, - () -> messageBatchRecoverer.recover(expectedMessage, expectedThrowable)); - - assertThat(expectedMessage).isEqualTo(exception.getFailedMessage()); - assertThat(exception.getCause()).isInstanceOf(AmqpRejectAndDontRequeueException.class); - assertThat(expectedThrowable).isEqualTo(exception.getCause().getCause()); - } -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java deleted file mode 100644 index 4cb59bcc3..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder.rabbit; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.testcontainers.containers.RabbitMQContainer; - -import org.springframework.amqp.core.Base64UrlNamingStrategy; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.TopicExchange; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.core.ChannelCallback; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.cloud.stream.binder.AbstractBinder; -import org.springframework.cloud.stream.binder.rabbit.admin.RabbitAdminException; -import org.springframework.cloud.stream.binder.rabbit.admin.RabbitBindingCleaner; -import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; -import org.springframework.core.ParameterizedTypeReference; -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.junit.Assert.fail; - -/** - * @author Gary Russell - * @author Chris Bono - * @since 1.2 - */ -public class RabbitBinderCleanerTests { - - private static final RabbitMQContainer RABBITMQ = RabbitTestContainer.sharedInstance(); - - private static final String BINDER_PREFIX = "binder."; - - private static final WebClient client; - - static { - client = WebClient.builder() - .filter(ExchangeFilterFunctions - .basicAuthentication(RABBITMQ.getAdminUsername(), RABBITMQ.getAdminPassword())) - .build(); - } - - @RegisterExtension - private final RabbitTestSupport rabbitTestSupport = new RabbitTestSupport(true, RABBITMQ.getAmqpPort(), - RABBITMQ.getHttpPort()); - - @Test - public void testCleanStream() { - final RabbitBindingCleaner cleaner = new RabbitBindingCleaner(); - final String stream1 = new Base64UrlNamingStrategy("foo").generateName(); - String stream2 = stream1 + "-1"; - String firstQueue = null; - CachingConnectionFactory connectionFactory = rabbitTestSupport.getResource(); - RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); - for (int i = 0; i < 5; i++) { - String queue1Name = AbstractBinder.applyPrefix(BINDER_PREFIX, - stream1 + ".default." + i); - String queue2Name = AbstractBinder.applyPrefix(BINDER_PREFIX, - stream2 + ".default." + i); - if (firstQueue == null) { - firstQueue = queue1Name; - } - rabbitAdmin.declareQueue(new Queue(queue1Name, true, false, false)); - rabbitAdmin.declareQueue(new Queue(queue2Name, true, false, false)); - rabbitAdmin.declareQueue(new Queue(AbstractBinder.constructDLQName(queue1Name), true, false, false)); - TopicExchange exchange = new TopicExchange(queue1Name); - rabbitAdmin.declareExchange(exchange); - rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue(queue1Name)) - .to(exchange).with(queue1Name)); - exchange = new TopicExchange(queue2Name); - rabbitAdmin.declareExchange(exchange); - rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue(queue2Name)) - .to(exchange).with(queue2Name)); - } - final TopicExchange topic1 = new TopicExchange( - AbstractBinder.applyPrefix(BINDER_PREFIX, stream1 + ".foo.bar")); - rabbitAdmin.declareExchange(topic1); - rabbitAdmin.declareBinding( - BindingBuilder.bind(new Queue(firstQueue)).to(topic1).with("#")); - String foreignQueue = UUID.randomUUID().toString(); - rabbitAdmin.declareQueue(new Queue(foreignQueue)); - rabbitAdmin.declareBinding( - BindingBuilder.bind(new Queue(foreignQueue)).to(topic1).with("#")); - final TopicExchange topic2 = new TopicExchange( - AbstractBinder.applyPrefix(BINDER_PREFIX, stream2 + ".foo.bar")); - rabbitAdmin.declareExchange(topic2); - rabbitAdmin.declareBinding( - BindingBuilder.bind(new Queue(firstQueue)).to(topic2).with("#")); - new RabbitTemplate(connectionFactory).execute(new ChannelCallback() { - - @Override - public Void doInRabbit(Channel channel) throws Exception { - String queueName = AbstractBinder.applyPrefix(BINDER_PREFIX, - stream1 + ".default." + 4); - String consumerTag = channel.basicConsume(queueName, - new DefaultConsumer(channel)); - try { - waitForConsumerState(queueName, 1); - doClean(cleaner, stream1, false); - fail("Expected exception"); - } - catch (RabbitAdminException e) { - assertThat(e) - .hasMessageContaining("Queue " + queueName + " is in use"); - } - channel.basicCancel(consumerTag); - waitForConsumerState(queueName, 0); - try { - doClean(cleaner, stream1, false); - fail("Expected exception"); - } - catch (RabbitAdminException e) { - assertThat(e).hasMessageContaining("Cannot delete exchange "); - assertThat(e).hasMessageContaining("; it has bindings:"); - } - return null; - } - - private void waitForConsumerState(String queueName, long state) - throws InterruptedException, URISyntaxException { - - int n = 0; - Map queue = getQueue("/", queueName); - while (n++ < 100 && !requiredState(state, queue)) { - Thread.sleep(100); - queue = getQueue("/", queueName); - } - assertThat(n).withFailMessage( - "Consumer state remained at " + state + " after 10 seconds") - .isLessThan(100); - } - - private boolean requiredState(long state, Map queue) { - Object consumers = queue.get("consumers"); - return state == 0 - ? consumers == null || (Integer) consumers == 0 - : consumers != null && (Integer) consumers == state; - } - - }); - rabbitAdmin.deleteExchange(topic1.getName()); // easier than deleting the binding - rabbitAdmin.declareExchange(topic1); - rabbitAdmin.deleteQueue(foreignQueue); - connectionFactory.destroy(); - Map> cleanedMap = doClean(cleaner, stream1, false); - assertThat(cleanedMap).hasSize(2); - List cleanedQueues = cleanedMap.get("queues"); - // should *not* clean stream2 - assertThat(cleanedQueues).hasSize(10); - for (int i = 0; i < 5; i++) { - assertThat(cleanedQueues.get(i * 2)) - .isEqualTo(BINDER_PREFIX + stream1 + ".default." + i); - assertThat(cleanedQueues.get(i * 2 + 1)) - .isEqualTo(BINDER_PREFIX + stream1 + ".default." + i + ".dlq"); - } - List cleanedExchanges = cleanedMap.get("exchanges"); - assertThat(cleanedExchanges).hasSize(6); - - // wild card *should* clean stream2 - cleanedMap = doClean(cleaner, stream1 + "*", false); - assertThat(cleanedMap).hasSize(2); - cleanedQueues = cleanedMap.get("queues"); - assertThat(cleanedQueues).hasSize(5); - for (int i = 0; i < 5; i++) { - assertThat(cleanedQueues.get(i)) - .isEqualTo(BINDER_PREFIX + stream2 + ".default." + i); - } - cleanedExchanges = cleanedMap.get("exchanges"); - assertThat(cleanedExchanges).hasSize(6); - } - - protected Map getQueue(String string, String queueName) throws URISyntaxException { - URI uri = new URI(RABBITMQ.getHttpUrl()) - .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queueName); - return client.get() - .uri(uri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(Duration.ofSeconds(10)); - } - - private static Map> doClean(RabbitBindingCleaner cleaner, String entity, boolean isJob) { - return cleaner.clean(RABBITMQ.getHttpUrl() + "/api", RABBITMQ.getAdminUsername(), RABBITMQ.getAdminPassword(), - "/", BINDER_PREFIX, entity, isJob); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java deleted file mode 100644 index c9b1dce43..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java +++ /dev/null @@ -1,2787 +0,0 @@ -/* - * Copyright 2013-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.cloud.stream.binder.rabbit; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.reflect.Constructor; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -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.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.zip.Deflater; - -import com.rabbitmq.client.LongString; -import org.apache.commons.logging.Log; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.ArgumentCaptor; -import org.testcontainers.containers.RabbitMQContainer; - -import org.springframework.amqp.AmqpIOException; -import org.springframework.amqp.ImmediateAcknowledgeAmqpException; -import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.AnonymousQueue; -import org.springframework.amqp.core.Binding.DestinationType; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Declarable; -import org.springframework.amqp.core.Declarables; -import org.springframework.amqp.core.DirectExchange; -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.core.MessageDeliveryMode; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.TopicExchange; -import org.springframework.amqp.rabbit.batch.BatchingStrategy; -import org.springframework.amqp.rabbit.batch.MessageBatch; -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.connection.CorrelationData; -import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; -import org.springframework.amqp.rabbit.connection.RabbitUtils; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; -import org.springframework.amqp.rabbit.listener.AsyncConsumerStartedEvent; -import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; -import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.cloud.stream.binder.BinderException; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; -import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy; -import org.springframework.cloud.stream.binder.PartitionSelectorStrategy; -import org.springframework.cloud.stream.binder.PartitionTestSupport; -import org.springframework.cloud.stream.binder.PollableSource; -import org.springframework.cloud.stream.binder.RequeueCurrentMessageException; -import org.springframework.cloud.stream.binder.Spy; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties.QuorumConfig; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.AlternateExchange; -import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; -import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.Lifecycle; -import org.springframework.expression.spel.standard.SpelExpression; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; -import org.springframework.integration.amqp.support.NackedAmqpMessageException; -import org.springframework.integration.amqp.support.ReturnedAmqpMessageException; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.expression.ValueExpression; -import org.springframework.integration.handler.BridgeHandler; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHandlingException; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.MimeTypeUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * @author Mark Fisher - * @author Gary Russell - * @author David Turanski - * @author Artem Bilan - * @author Chris Bono - */ -// @checkstyle:off -public class RabbitBinderTests extends - PartitionCapableBinderTests, ExtendedProducerProperties> { - - protected static final RabbitMQContainer RABBITMQ = RabbitTestContainer.sharedInstance(); - - private static final String CLASS_UNDER_TEST_NAME = RabbitMessageChannelBinder.class.getSimpleName(); - - private static final String TEST_PREFIX = "bindertest."; - - private static final String BIG_EXCEPTION_MESSAGE = new String(new byte[10_000]).replaceAll("\u0000", "x"); - - private static final WebClient client; - - private static final URI uri; - - static { - client = WebClient.builder() - .filter(ExchangeFilterFunctions - .basicAuthentication(RABBITMQ.getAdminUsername(), RABBITMQ.getAdminPassword())) - .build(); - try { - uri = new URI(RABBITMQ.getHttpUrl() + "/api/"); - } - catch (URISyntaxException ex) { - throw new IllegalStateException(ex); - } - } - - @RegisterExtension - private final RabbitTestSupport rabbitTestSupport = new RabbitTestSupport(true, RABBITMQ.getAmqpPort(), RABBITMQ.getHttpPort()); - - private int maxStackTraceSize; - - @Override - protected RabbitTestBinder getBinder() { - if (this.testBinder == null) { - RabbitProperties rabbitProperties = new RabbitProperties(); - this.testBinder = new RabbitTestBinder(this.rabbitTestSupport.getResource(), rabbitProperties); - } - return this.testBinder; - } - - @Override - protected ExtendedConsumerProperties createConsumerProperties() { - return new ExtendedConsumerProperties<>(new RabbitConsumerProperties()); - } - - @Override - protected ExtendedProducerProperties createProducerProperties(TestInfo testInfo) { - ExtendedProducerProperties props = new ExtendedProducerProperties<>( - new RabbitProducerProperties()); - - if (testInfo.getTestMethod().get().getName().equals("testPartitionedModuleSpEL")) { - props.getExtension().setRoutingKeyExpression( - spelExpressionParser.parseExpression("'part.0'")); - } - return props; - } - - @Override - protected boolean usesExplicitRouting() { - return true; - } - - @Test - public void testSendAndReceiveBad(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - final AtomicReference event = new AtomicReference<>(); - binder.getApplicationContext().addApplicationListener( - (ApplicationListener) e -> event.set(e)); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - DirectChannel moduleInputChannel = createBindableChannel("input", - new BindingProperties()); - Binding producerBinding = binder.bindProducer("bad.0", - moduleOutputChannel, createProducerProperties(testInfo)); - assertThat(TestUtils.getPropertyValue(producerBinding, - "lifecycle.headersMappedLast", Boolean.class)).isTrue(); - assertThat( - TestUtils - .getPropertyValue(producerBinding, - "lifecycle.amqpTemplate.messageConverter") - .getClass().getName()).contains("Passthrough"); - ExtendedConsumerProperties consumerProps = createConsumerProperties(); - consumerProps.getExtension().setContainerType(ContainerType.DIRECT); - Binding consumerBinding = binder.bindConsumer("bad.0", "test", - moduleInputChannel, consumerProps); - assertThat( - TestUtils.getPropertyValue(consumerBinding, "lifecycle.messageConverter") - .getClass().getName()).contains("Passthrough"); - assertThat(TestUtils.getPropertyValue(consumerBinding, - "lifecycle.messageListenerContainer")) - .isInstanceOf(DirectMessageListenerContainer.class); - Message message = MessageBuilder.withPayload("bad".getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); - final CountDownLatch latch = new CountDownLatch(3); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - latch.countDown(); - throw new RuntimeException("bad"); - } - }); - moduleOutputChannel.send(message); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(event.get()).isNotNull(); - producerBinding.unbind(); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testProducerErrorChannel(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - CachingConnectionFactory ccf = this.rabbitTestSupport.getResource(); - ccf.setPublisherReturns(true); - ccf.setPublisherConfirmType(ConfirmType.CORRELATED); - ccf.resetConnection(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = createProducerProperties(testInfo); - producerProps.setErrorChannelEnabled(true); - producerProps.populateBindingName("output"); - Binding producerBinding = binder.bindProducer("ec.0", - moduleOutputChannel, producerProps); - final Message message = MessageBuilder.withPayload("bad".getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); - String s = testBinder.getBinder().getBinderIdentity() + ".output.errors"; - SubscribableChannel ec = binder.getApplicationContext().getBean(s, - SubscribableChannel.class); - final AtomicReference> errorMessage = new AtomicReference<>(); - final CountDownLatch latch = new CountDownLatch(2); - ec.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - errorMessage.set(message); - latch.countDown(); - } - - }); - SubscribableChannel globalEc = binder.getApplicationContext().getBean( - IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, - SubscribableChannel.class); - globalEc.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - latch.countDown(); - } - - }); - moduleOutputChannel.send(message); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(errorMessage.get()).isInstanceOf(ErrorMessage.class); - assertThat(errorMessage.get().getPayload()) - .isInstanceOf(ReturnedAmqpMessageException.class); - ReturnedAmqpMessageException exception = (ReturnedAmqpMessageException) errorMessage - .get().getPayload(); - assertThat(exception.getReplyCode()).isEqualTo(312); - assertThat(exception.getReplyText()).isEqualTo("NO_ROUTE"); - - AmqpOutboundEndpoint endpoint = TestUtils.getPropertyValue(producerBinding, - "lifecycle", AmqpOutboundEndpoint.class); - assertThat(TestUtils.getPropertyValue(endpoint, - "confirmCorrelationExpression.expression")).isEqualTo("#root"); - class WrapperAccessor extends AmqpOutboundEndpoint { - - WrapperAccessor(AmqpTemplate amqpTemplate) { - super(amqpTemplate); - } - - CorrelationDataWrapper getWrapper() throws Exception { - Constructor constructor = CorrelationDataWrapper.class - .getDeclaredConstructor(String.class, Object.class, - Message.class); - ReflectionUtils.makeAccessible(constructor); - return constructor.newInstance(UUID.randomUUID().toString(), message, message); - } - - } - endpoint.confirm(new WrapperAccessor(mock(AmqpTemplate.class)).getWrapper(), - false, "Mock NACK"); - assertThat(errorMessage.get()).isInstanceOf(ErrorMessage.class); - assertThat(errorMessage.get().getPayload()) - .isInstanceOf(NackedAmqpMessageException.class); - NackedAmqpMessageException nack = (NackedAmqpMessageException) errorMessage.get() - .getPayload(); - assertThat(nack.getNackReason()).isEqualTo("Mock NACK"); - assertThat(nack.getCorrelationData()).isEqualTo(message); - assertThat(nack.getFailedMessage()).isEqualTo(message); - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testProducerAckChannel(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - CachingConnectionFactory ccf = this.rabbitTestSupport.getResource(); - ccf.setPublisherReturns(true); - ccf.setPublisherConfirmType(ConfirmType.CORRELATED); - ccf.resetConnection(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = createProducerProperties(testInfo); - producerProps.setErrorChannelEnabled(true); - producerProps.getExtension().setConfirmAckChannel("acksChannel"); - Binding producerBinding = binder.bindProducer("acks.0", - moduleOutputChannel, producerProps); - final Message message = MessageBuilder.withPayload("acksMessage".getBytes()) - .build(); - final AtomicReference> confirm = new AtomicReference<>(); - final CountDownLatch confirmLatch = new CountDownLatch(1); - binder.getApplicationContext().getBean("acksChannel", DirectChannel.class) - .subscribe(m -> { - confirm.set(m); - confirmLatch.countDown(); - }); - moduleOutputChannel.send(message); - assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(confirm.get().getPayload()).isEqualTo("acksMessage".getBytes()); - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testProducerConfirmHeader(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - CachingConnectionFactory ccf = this.rabbitTestSupport.getResource(); - ccf.setPublisherReturns(true); - ccf.setPublisherConfirmType(ConfirmType.CORRELATED); - ccf.resetConnection(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = createProducerProperties(testInfo); - producerProps.getExtension().setUseConfirmHeader(true); - Binding producerBinding = binder.bindProducer("confirms.0", - moduleOutputChannel, producerProps); - CorrelationData correlation = new CorrelationData("testConfirm"); - final Message message = MessageBuilder.withPayload("confirmsMessage".getBytes()) - .setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, correlation) - .build(); - moduleOutputChannel.send(message); - Confirm confirm = correlation.getFuture().get(10, TimeUnit.SECONDS); - assertThat(confirm.isAck()).isTrue(); - assertThat(correlation.getReturned()).isNotNull(); - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - void producerWithAlternateExchange(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - DirectChannel moduleOutputChannel = createBindableChannel("output", - new BindingProperties()); - ExtendedProducerProperties producerProps = createProducerProperties(testInfo); - AlternateExchange alternate = new AlternateExchange(); - alternate.setName("altEx"); - RabbitProducerProperties.AlternateExchange.Binding binding = - new RabbitProducerProperties.AlternateExchange.Binding(); - binding.setQueue("altQ"); - alternate.setBinding(binding); - producerProps.getExtension().setAlternateExchange(alternate); - Binding producerBinding = binder.bindProducer("alt.0", - moduleOutputChannel, producerProps); - final Message message = MessageBuilder.withPayload("altMessage".getBytes()) - .build(); - moduleOutputChannel.send(message); - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - RabbitTemplate template = new RabbitTemplate(this.rabbitTestSupport.getResource()); - Object received = template.receiveAndConvert("altQ", 10_000); - assertThat(received).isEqualTo("altMessage".getBytes()); - RabbitAdmin admin = new RabbitAdmin(template.getConnectionFactory()); - admin.deleteQueue("altQ"); - admin.deleteExchange("altEx"); - } - - @Test - public void testConsumerProperties() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setRequeueRejected(true); - properties.getExtension().setTransacted(true); - properties.getExtension().setExclusive(true); - properties.getExtension().setMissingQueuesFatal(true); - properties.getExtension().setFailedDeclarationRetryInterval(1500L); - properties.getExtension().setQueueDeclarationRetries(23); - Binding consumerBinding = binder.bindConsumer("props.0", null, - createBindableChannel("input", new BindingProperties()), properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - assertThat(container.getAcknowledgeMode()).isEqualTo(AcknowledgeMode.AUTO); - assertThat(container.getQueueNames()[0]) - .startsWith(properties.getExtension().getPrefix()); - assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)) - .isTrue(); - assertThat(TestUtils.getPropertyValue(container, "exclusive", Boolean.class)) - .isTrue(); - assertThat(TestUtils.getPropertyValue(container, "concurrentConsumers")) - .isEqualTo(1); - assertThat(TestUtils.getPropertyValue(container, "maxConcurrentConsumers")) - .isNull(); - assertThat(TestUtils.getPropertyValue(container, "defaultRequeueRejected", - Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(container, "prefetchCount")).isEqualTo(1); - assertThat(TestUtils.getPropertyValue(container, "batchSize")).isEqualTo(1); - assertThat(TestUtils.getPropertyValue(container, "missingQueuesFatal", - Boolean.class)).isTrue(); - assertThat( - TestUtils.getPropertyValue(container, "failedDeclarationRetryInterval")) - .isEqualTo(1500L); - assertThat(TestUtils.getPropertyValue(container, "declarationRetries")) - .isEqualTo(23); - RetryTemplate retry = TestUtils.getPropertyValue(endpoint, "retryTemplate", - RetryTemplate.class); - assertThat(TestUtils.getPropertyValue(retry, "retryPolicy.maxAttempts")) - .isEqualTo(3); - assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.initialInterval")) - .isEqualTo(1000L); - assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.maxInterval")) - .isEqualTo(10000L); - assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.multiplier")) - .isEqualTo(2.0); - consumerBinding.unbind(); - assertThat(endpoint.isRunning()).isFalse(); - - properties = createConsumerProperties(); - properties.getExtension().setAcknowledgeMode(AcknowledgeMode.NONE); - properties.setBackOffInitialInterval(2000); - properties.setBackOffMaxInterval(20000); - properties.setBackOffMultiplier(5.0); - properties.setConcurrency(2); - properties.setMaxAttempts(23); - properties.getExtension().setMaxConcurrency(3); - properties.getExtension().setPrefix("foo."); - properties.getExtension().setPrefetch(20); - properties.getExtension().setHeaderPatterns(new String[] { "foo" }); - properties.getExtension().setTxSize(10); - QuorumConfig quorum = properties.getExtension().getQuorum(); - quorum.setEnabled(true); - quorum.setDeliveryLimit(10); - quorum.setInitialGroupSize(1); - properties.setInstanceIndex(0); - consumerBinding = binder.bindConsumer("props.0", "test", - createBindableChannel("input", new BindingProperties()), properties); - - endpoint = extractEndpoint(consumerBinding); - container = verifyContainer(endpoint); - - - assertThat(container.getQueueNames()[0]).isEqualTo("foo.props.0.test"); - - consumerBinding.unbind(); - assertThat(endpoint.isRunning()).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testMultiplexOnPartitionedConsumer() throws Exception { - final ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); - CachingConnectionFactory cf = new CachingConnectionFactory("localhost", - proxy.getPort()); - - final RabbitExchangeQueueProvisioner rabbitExchangeQueueProvisioner = new RabbitExchangeQueueProvisioner(cf); - - consumerProperties.setMultiplex(true); - consumerProperties.setPartitioned(true); - consumerProperties.setInstanceIndexList(Arrays.asList(1, 2, 3)); - - final ConsumerDestination consumerDestination = rabbitExchangeQueueProvisioner.provisionConsumerDestination("foo", "boo", consumerProperties); - - final String name = consumerDestination.getName(); - - assertThat(name).isEqualTo("foo.boo-1,foo.boo-2,foo.boo-3"); - } - - @Test - public void testMultiplexOnPartitionedConsumerWithMultipleDestinations() throws Exception { - final ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); - CachingConnectionFactory cf = new CachingConnectionFactory("localhost", - proxy.getPort()); - - final RabbitExchangeQueueProvisioner rabbitExchangeQueueProvisioner = new RabbitExchangeQueueProvisioner(cf); - - consumerProperties.setMultiplex(true); - consumerProperties.setPartitioned(true); - consumerProperties.setInstanceIndexList(Arrays.asList(1, 2, 3)); - - final ConsumerDestination consumerDestination = rabbitExchangeQueueProvisioner.provisionConsumerDestination("foo,qaa", "boo", consumerProperties); - - final String name = consumerDestination.getName(); - - assertThat(name).isEqualTo("foo.boo-1,foo.boo-2,foo.boo-3,qaa.boo-1,qaa.boo-2,qaa.boo-3"); - } - - @Test - public void testConsumerPropertiesWithUserInfrastructureNoBind() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.rabbitTestSupport.getResource()); - Queue queue = new Queue("propsUser1.infra"); - admin.declareQueue(queue); - DirectExchange exchange = new DirectExchange("propsUser1"); - admin.declareExchange(exchange); - admin.declareBinding(BindingBuilder.bind(queue).to(exchange).with("foo")); - - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setDeclareExchange(false); - properties.getExtension().setBindQueue(false); - - Binding consumerBinding = binder.bindConsumer("propsUser1", - "infra", createBindableChannel("input", new BindingProperties()), - properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - assertThat(TestUtils.getPropertyValue(container, "missingQueuesFatal", - Boolean.class)).isFalse(); - assertThat(container.isRunning()).isTrue(); - consumerBinding.unbind(); - assertThat(container.isRunning()).isFalse(); - List bindings = getBindingsBySource("/", exchange.getName()); - assertThat(bindings.size()).isEqualTo(1); - } - - @Test - public void testAnonWithBuiltInExchange() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setDeclareExchange(false); - properties.getExtension().setQueueNameGroupOnly(true); - - Binding consumerBinding = binder.bindConsumer("amq.topic", null, - createBindableChannel("input", new BindingProperties()), properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - String queueName = container.getQueueNames()[0]; - assertThat(queueName).startsWith("anonymous."); - assertThat(container.isRunning()).isTrue(); - consumerBinding.unbind(); - assertThat(container.isRunning()).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testAnonWithBuiltInExchangeCustomPrefix() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setDeclareExchange(false); - properties.getExtension().setQueueNameGroupOnly(true); - properties.getExtension().setAnonymousGroupPrefix("customPrefix."); - - Binding consumerBinding = binder.bindConsumer("amq.topic", null, - createBindableChannel("input", new BindingProperties()), properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - String queueName = container.getQueueNames()[0]; - assertThat(queueName).startsWith("customPrefix."); - assertThat(container.isRunning()).isTrue(); - consumerBinding.unbind(); - assertThat(container.isRunning()).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testConsumerPropertiesWithUserInfrastructureCustomExchangeAndRK() - throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setExchangeType(ExchangeTypes.DIRECT); - properties.getExtension().setBindingRoutingKey("foo,bar"); - properties.getExtension().setBindingRoutingKeyDelimiter(","); - properties.getExtension().setQueueNameGroupOnly(true); - // properties.getExtension().setDelayedExchange(true); // requires delayed message - // exchange plugin; tested locally - - String group = "infra"; - Binding consumerBinding = binder.bindConsumer("propsUser2", group, - createBindableChannel("input", new BindingProperties()), properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - assertThat(container.isRunning()).isTrue(); - ApplicationContext ctx = - TestUtils.getPropertyValue(binder, "binder.provisioningProvider.autoDeclareContext", - ApplicationContext.class); - assertThat(ctx.getBean("infra.binding", Declarables.class).getDeclarables()).hasSize(2); - consumerBinding.unbind(); - assertThat(container.isRunning()).isFalse(); - assertThat(container.getQueueNames()[0]).isEqualTo(group); - List> bindings = getBindingsBySource("/", "propsUser2"); - int n = 0; - while (n++ < 100 && bindings == null || bindings.size() < 2) { - Thread.sleep(100); - bindings = getBindingsBySource("/", "propsUser2"); - } - assertThat(bindings.size()).isEqualTo(2); - assertThat(bindings.get(0).get("source")).isEqualTo("propsUser2"); - assertThat(bindings.get(0).get("destination")).isEqualTo(group); - assertThat(bindings.get(0).get("routing_key")).isIn("foo", "bar"); - assertThat(bindings.get(1).get("source")).isEqualTo("propsUser2"); - assertThat(bindings.get(1).get("destination")).isEqualTo(group); - assertThat(bindings.get(1).get("routing_key")).isIn("foo", "bar"); - assertThat(bindings.get(1).get("routing_key")).isNotEqualTo(bindings.get(0).get("routing_key")); - - Map exchange = getExchange("/", "propsUser2"); - while (n++ < 100 && exchange == null) { - Thread.sleep(100); - exchange = getExchange("/", "propsUser2"); - } - assertThat(exchange.get("type")).isEqualTo("direct"); - assertThat(exchange.get("durable")).isEqualTo(true); - assertThat(exchange.get("auto_delete")).isEqualTo(false); - - verifyAutoDeclareContextClear(binder); - } - - private String adminUri() { - return String.format("http://guest:guest@localhost:%d/api", RABBITMQ.getHttpPort()); - } - - @Test - public void testConsumerPropertiesWithUserInfrastructureCustomQueueArgs() - throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - RabbitConsumerProperties extProps = properties.getExtension(); - extProps.setExchangeType(ExchangeTypes.DIRECT); - extProps.setExchangeDurable(false); - extProps.setExchangeAutoDelete(true); - extProps.setBindingRoutingKey("foo"); - extProps.setExpires(30_000); - extProps.setLazy(true); - extProps.setMaxLength(10_000); - extProps.setMaxLengthBytes(100_000); - extProps.setMaxPriority(10); - extProps.setOverflowBehavior("drop-head"); - extProps.setTtl(2_000); - extProps.setAutoBindDlq(true); - extProps.setDeadLetterQueueName("customDLQ"); - extProps.setDeadLetterExchange("customDLX"); - extProps.setDeadLetterExchangeType(ExchangeTypes.TOPIC); - extProps.setDeadLetterRoutingKey("customDLRK"); - extProps.setDlqDeadLetterExchange("propsUser3"); - // GH-259 - if the next line was commented, the test failed. - extProps.setDlqDeadLetterRoutingKey("propsUser3"); - extProps.setDlqExpires(60_000); - extProps.setDlqLazy(true); - extProps.setDlqMaxLength(20_000); - extProps.setDlqMaxLengthBytes(40_000); - extProps.setDlqOverflowBehavior("reject-publish"); - extProps.setDlqMaxPriority(8); - extProps.setDlqTtl(1_000); - extProps.setConsumerTagPrefix("testConsumerTag"); - extProps.setExclusive(true); - - Binding consumerBinding = binder.bindConsumer("propsUser3", - "infra", createBindableChannel("input", new BindingProperties()), - properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - assertThat(container.isRunning()).isTrue(); - List> bindings = getBindingsBySource("/", "propsUser3"); - int n = 0; - while (n++ < 100 && bindings == null || bindings.size() < 1) { - Thread.sleep(100); - bindings = getBindingsBySource("/", "propsUser3"); - } - assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("source")).isEqualTo("propsUser3"); - assertThat(bindings.get(0).get("destination")).isEqualTo("propsUser3.infra"); - assertThat(bindings.get(0).get("routing_key")).isEqualTo("foo"); - - bindings = getBindingsBySource("/", "customDLX"); - n = 0; - while (n++ < 100 && bindings == null || bindings.size() < 1) { - Thread.sleep(100); - bindings = getBindingsBySource("/", "customDLX"); - } -// assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("source")).isEqualTo("customDLX"); - assertThat(bindings.get(0).get("destination")).isEqualTo("customDLQ"); - assertThat(bindings.get(0).get("routing_key")).isEqualTo("customDLRK"); - - Map exchange = getExchange("/", "propsUser3"); - n = 0; - while (n++ < 100 && exchange == null) { - Thread.sleep(100); - exchange = getExchange("/", "propsUser3"); - } - assertThat(exchange.get("type")).isEqualTo("direct"); - assertThat(exchange.get("durable")).isEqualTo(false); - assertThat(exchange.get("auto_delete")).isEqualTo(true); - - exchange = getExchange("/", "customDLX"); - n = 0; - while (n++ < 100 && exchange == null) { - Thread.sleep(100); - exchange = getExchange("/", "customDLX"); - } - assertThat(exchange.get("type")).isEqualTo("topic"); - assertThat(exchange.get("durable")).isEqualTo(true); - assertThat(exchange.get("auto_delete")).isEqualTo(false); - - Map queue = getQueue("/", "propsUser3.infra"); - n = 0; - while (n++ < 100 && queue == null || !requiredConsumerState(1, queue)) { - Thread.sleep(100); - queue = getQueue("/", "propsUser3.infra"); - } - assertThat(queue).isNotNull(); - Map args = (Map) queue.get("arguments"); - assertThat(args.get("x-expires")).isEqualTo(30_000); - assertThat(args.get("x-max-length")).isEqualTo(10_000); - assertThat(args.get("x-max-length-bytes")).isEqualTo(100_000); - assertThat(args.get("x-overflow")).isEqualTo("drop-head"); - assertThat(args.get("x-max-priority")).isEqualTo(10); - assertThat(args.get("x-message-ttl")).isEqualTo(2_000); - assertThat(args.get("x-dead-letter-exchange")).isEqualTo("customDLX"); - assertThat(args.get("x-dead-letter-routing-key")).isEqualTo("customDLRK"); - assertThat(args.get("x-queue-mode")).isEqualTo("lazy"); - assertThat(queue.get("exclusive_consumer_tag")).isEqualTo("testConsumerTag#0"); - - queue = getQueue("/", "customDLQ"); - - n = 0; - while (n++ < 100 && queue == null) { - Thread.sleep(100); - queue = getQueue("/", "customDLQ"); - } - assertThat(queue).isNotNull(); - args = (Map) queue.get("arguments"); - assertThat(args.get("x-expires")).isEqualTo(60_000); - assertThat(args.get("x-max-length")).isEqualTo(20_000); - assertThat(args.get("x-max-length-bytes")).isEqualTo(40_000); - assertThat(args.get("x-overflow")).isEqualTo("reject-publish"); - assertThat(args.get("x-max-priority")).isEqualTo(8); - assertThat(args.get("x-message-ttl")).isEqualTo(1_000); - assertThat(args.get("x-dead-letter-exchange")).isEqualTo("propsUser3"); - assertThat(args.get("x-dead-letter-routing-key")).isEqualTo("propsUser3"); - assertThat(args.get("x-queue-mode")).isEqualTo("lazy"); - - consumerBinding.unbind(); - assertThat(container.isRunning()).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testConsumerPropertiesWithHeaderExchanges() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setExchangeType(ExchangeTypes.HEADERS); - properties.getExtension().setAutoBindDlq(true); - properties.getExtension().setDeadLetterExchange(ExchangeTypes.HEADERS); - properties.getExtension().setDeadLetterExchange("propsHeader.dlx"); - Map queueBindingArguments = new HashMap<>(); - queueBindingArguments.put("x-match", "any"); - queueBindingArguments.put("foo", "bar"); - properties.getExtension().setQueueBindingArguments(queueBindingArguments); - properties.getExtension().setDlqBindingArguments(queueBindingArguments); - - String group = "bindingArgs"; - Binding consumerBinding = binder.bindConsumer("propsHeader", group, - createBindableChannel("input", new BindingProperties()), properties); - Lifecycle endpoint = extractEndpoint(consumerBinding); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, - "messageListenerContainer", SimpleMessageListenerContainer.class); - assertThat(container.isRunning()).isTrue(); - consumerBinding.unbind(); - assertThat(container.isRunning()).isFalse(); - assertThat(container.getQueueNames()[0]).isEqualTo("propsHeader." + group); - List> bindings = getBindingsBySource("/", "propsHeader"); - int n = 0; - while (n++ < 100 && bindings == null || bindings.size() < 1) { - Thread.sleep(100); - bindings = getBindingsBySource("/", "propsHeader"); - } - assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("source")).isEqualTo("propsHeader"); - assertThat(bindings.get(0).get("destination")).isEqualTo("propsHeader." + group); - Map args = (Map) bindings.get(0).get("arguments"); - assertThat(args).hasEntrySatisfying("x-match", v -> assertThat(v).isEqualTo("any")); - assertThat(args).hasEntrySatisfying("foo", v -> assertThat(v).isEqualTo("bar")); - - bindings = getBindingsBySource("/", "propsHeader.dlx"); - n = 0; - while (n++ < 100 && bindings == null || bindings.size() < 1) { - Thread.sleep(100); - bindings = getBindingsBySource("/", "propsHeader.dlx"); - } - assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("source")).isEqualTo("propsHeader.dlx"); - assertThat(bindings.get(0).get("destination")).isEqualTo("propsHeader." + group + ".dlq"); - args = (Map) bindings.get(0).get("arguments"); - assertThat(args).hasEntrySatisfying("x-match", v -> assertThat(v).isEqualTo("any")); - assertThat(args).hasEntrySatisfying("foo", v -> assertThat(v).isEqualTo("bar")); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testProducerProperties(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - Binding producerBinding = binder.bindProducer("props.0", - createBindableChannel("input", new BindingProperties()), - createProducerProperties(testInfo)); - Lifecycle endpoint = extractEndpoint(producerBinding); - MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, - "defaultDeliveryMode", MessageDeliveryMode.class); - assertThat(mode).isEqualTo(MessageDeliveryMode.PERSISTENT); - List requestHeaders = TestUtils.getPropertyValue(endpoint, - "headerMapper.requestHeaderMatcher.matchers", List.class); - assertThat(requestHeaders).hasSize(5); - producerBinding.unbind(); - assertThat(endpoint.isRunning()).isFalse(); - assertThat(TestUtils.getPropertyValue(endpoint, "amqpTemplate.transactional", - Boolean.class)).isFalse(); - - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - this.applicationContext.registerBean("pkExtractor", - TestPartitionKeyExtractorClass.class, () -> new TestPartitionKeyExtractorClass()); - this.applicationContext.registerBean("pkSelector", - TestPartitionSelectorClass.class, () -> new TestPartitionSelectorClass()); - producerProperties.setPartitionKeyExtractorName("pkExtractor"); - producerProperties.setPartitionSelectorName("pkSelector"); - producerProperties.getExtension().setPrefix("foo."); - producerProperties.getExtension() - .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - producerProperties.getExtension().setHeaderPatterns(new String[] { "foo" }); - producerProperties - .setPartitionKeyExpression(spelExpressionParser.parseExpression("'foo'")); - producerProperties.setPartitionSelectorExpression( - spelExpressionParser.parseExpression("0")); - producerProperties.setPartitionCount(1); - producerProperties.getExtension().setTransacted(true); - producerProperties.getExtension() - .setDelayExpression(spelExpressionParser.parseExpression("42")); - producerProperties.setRequiredGroups("prodPropsRequired"); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - producerProperties); - DirectChannel channel = createBindableChannel("output", - producerBindingProperties); - producerBinding = binder.bindProducer("props.0", channel, producerProperties); - - ConnectionFactory producerConnectionFactory = TestUtils.getPropertyValue( - producerBinding, "lifecycle.amqpTemplate.connectionFactory", - ConnectionFactory.class); - - assertThat(this.rabbitTestSupport.getResource()) - .isSameAs(producerConnectionFactory); - - endpoint = extractEndpoint(producerBinding); - assertThat(getEndpointRouting(endpoint)).isEqualTo( - "'props.0-' + headers['" + BinderHeaders.PARTITION_HEADER + "']"); - assertThat(TestUtils - .getPropertyValue(endpoint, "delayExpression", SpelExpression.class) - .getExpressionString()).isEqualTo("42"); - mode = TestUtils.getPropertyValue(endpoint, "defaultDeliveryMode", - MessageDeliveryMode.class); - assertThat(mode).isEqualTo(MessageDeliveryMode.NON_PERSISTENT); - assertThat(TestUtils.getPropertyValue(endpoint, "amqpTemplate.transactional", - Boolean.class)).isTrue(); - verifyFooRequestProducer(endpoint); - channel.send(new GenericMessage<>("foo")); - org.springframework.amqp.core.Message received = new RabbitTemplate( - this.rabbitTestSupport.getResource()) - .receive("foo.props.0.prodPropsRequired-0", 10_000); - assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(42); - - producerBinding.unbind(); - assertThat(endpoint.isRunning()).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testDurablePubSubWithAutoBindDLQ() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.rabbitTestSupport.getResource()); - - RabbitTestBinder binder = getBinder(); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setDurableSubscription(true); - consumerProperties.setMaxAttempts(1); // disable retry - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - moduleInputChannel.setBeanName("durableTest"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - throw new RuntimeException("foo"); - } - - }); - Binding consumerBinding = binder.bindConsumer("durabletest.0", - "tgroup", moduleInputChannel, consumerProperties); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend(TEST_PREFIX + "durabletest.0", "", "foo"); - - int n = 0; - while (n++ < 100) { - Object deadLetter = template - .receiveAndConvert(TEST_PREFIX + "durabletest.0.tgroup.dlq"); - if (deadLetter != null) { - assertThat(deadLetter).isEqualTo("foo"); - break; - } - Thread.sleep(100); - } - assertThat(n).isLessThan(100); - - consumerBinding.unbind(); - assertThat(admin.getQueueProperties(TEST_PREFIX + "durabletest.0.tgroup.dlq")) - .isNotNull(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testNonDurablePubSubWithAutoBindDLQ() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.rabbitTestSupport.getResource()); - - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setDurableSubscription(false); - consumerProperties.setMaxAttempts(1); // disable retry - BindingProperties bindingProperties = createConsumerBindingProperties( - consumerProperties); - DirectChannel moduleInputChannel = createBindableChannel("input", - bindingProperties); - moduleInputChannel.setBeanName("nondurabletest"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - throw new RuntimeException("foo"); - } - - }); - Binding consumerBinding = binder.bindConsumer("nondurabletest.0", - "tgroup", moduleInputChannel, consumerProperties); - - consumerBinding.unbind(); - assertThat(admin.getQueueProperties(TEST_PREFIX + "nondurabletest.0.dlq")) - .isNull(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testAutoBindDLQ() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.setMaxAttempts(1); // disable retry - consumerProperties.getExtension().setDurableSubscription(true); - BindingProperties bindingProperties = createConsumerBindingProperties( - consumerProperties); - DirectChannel moduleInputChannel = createBindableChannel("input", - bindingProperties); - moduleInputChannel.setBeanName("dlqTest"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - throw new RuntimeException("foo"); - } - - }); - consumerProperties.setMultiplex(true); - Binding consumerBinding = binder.bindConsumer("dlqtest,dlqtest2", - "default", moduleInputChannel, consumerProperties); - AbstractMessageListenerContainer container = TestUtils.getPropertyValue( - consumerBinding, "lifecycle.messageListenerContainer", - AbstractMessageListenerContainer.class); - assertThat(container.getQueueNames().length).isEqualTo(2); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("", TEST_PREFIX + "dlqtest.default", "foo"); - - int n = 0; - while (n++ < 100) { - Object deadLetter = template - .receiveAndConvert(TEST_PREFIX + "dlqtest.default.dlq"); - if (deadLetter != null) { - assertThat(deadLetter).isEqualTo("foo"); - break; - } - Thread.sleep(100); - } - assertThat(n).isLessThan(100); - - template.convertAndSend("", TEST_PREFIX + "dlqtest2.default", "bar"); - - n = 0; - while (n++ < 100) { - Object deadLetter = template - .receiveAndConvert(TEST_PREFIX + "dlqtest2.default.dlq"); - if (deadLetter != null) { - assertThat(deadLetter).isEqualTo("bar"); - break; - } - Thread.sleep(100); - } - assertThat(n).isLessThan(100); - - consumerBinding.unbind(); - - ApplicationContext context = TestUtils.getPropertyValue(binder, - "binder.provisioningProvider.autoDeclareContext", - ApplicationContext.class); - assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.binding")) - .isFalse(); - assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default")).isFalse(); - assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.dlq.binding")) - .isFalse(); - assertThat(context.containsBean(TEST_PREFIX + "dlqtest.default.dlq")).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testAutoBindDLQManualAcks() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.setMaxAttempts(2); - consumerProperties.getExtension().setDurableSubscription(true); - consumerProperties.getExtension().setAcknowledgeMode(AcknowledgeMode.MANUAL); - BindingProperties bindingProperties = createConsumerBindingProperties( - consumerProperties); - DirectChannel moduleInputChannel = createBindableChannel("input", - bindingProperties); - moduleInputChannel.setBeanName("dlqTestManual"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - // Wait until the unacked state is reflected in the admin - Map info = getQueue("/", TEST_PREFIX + "dlqTestManual.default"); - int n = 0; - while (n++ < 100 && ( info.get("messages_unacknowledged") == null - || ((Long) info.get("messages_unacknowledged")) < 1L)) { - try { - Thread.sleep(100); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - info = getQueue("/", TEST_PREFIX + "dlqTestManual.default"); - } - throw new RuntimeException("foo"); - } - - }); - Binding consumerBinding = binder.bindConsumer("dlqTestManual", - "default", moduleInputChannel, consumerProperties); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("", TEST_PREFIX + "dlqTestManual.default", "foo"); - - int n = 0; - while (n++ < 100) { - Object deadLetter = template - .receiveAndConvert(TEST_PREFIX + "dlqTestManual.default.dlq"); - if (deadLetter != null) { - assertThat(deadLetter).isEqualTo("foo"); - break; - } - Thread.sleep(100); - } - assertThat(n).isLessThan(100); - - n = 0; - Map info = getQueue("/", TEST_PREFIX + "dlqTestManual.default"); - while (n++ < 100 && unackedMessages(info)) { - Thread.sleep(100); - info = getQueue("/", TEST_PREFIX + "dlqTestManual.default"); - } - assertThat(unackedMessages(info)).isFalse(); - - consumerBinding.unbind(); - - ApplicationContext context = TestUtils.getPropertyValue(binder, - "binder.provisioningProvider.autoDeclareContext", - ApplicationContext.class); - assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.binding")) - .isFalse(); - assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default")).isFalse(); - assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.dlq.binding")) - .isFalse(); - assertThat(context.containsBean(TEST_PREFIX + "dlqTestManual.default.dlq")).isFalse(); - - verifyAutoDeclareContextClear(binder); - } - - protected boolean unackedMessages(Map info) { - Object unack = info.get("messages_unacknowledged"); - return unack != null && ((Integer) unack) > 0L; - } - - - @Test - public void testAutoBindDLQPartionedConsumerFirst(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setPrefix("bindertest."); - properties.getExtension().setAutoBindDlq(true); - properties.setMaxAttempts(1); // disable retry - properties.setPartitioned(true); - properties.setInstanceIndex(0); - DirectChannel input0 = createBindableChannel("input", - createConsumerBindingProperties(properties)); - input0.setBeanName("test.input0DLQ"); - Binding input0Binding = binder.bindConsumer("partDLQ.0", - "dlqPartGrp", input0, properties); - Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.0", - "default", new QueueChannel(), properties); - properties.setInstanceIndex(1); - DirectChannel input1 = createBindableChannel("input1", - createConsumerBindingProperties(properties)); - input1.setBeanName("test.input1DLQ"); - Binding input1Binding = binder.bindConsumer("partDLQ.0", - "dlqPartGrp", input1, properties); - Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.0", - "default", new QueueChannel(), properties); - - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setPrefix("bindertest."); - this.applicationContext.registerBean("pkExtractor", PartitionTestSupport.class, () -> new PartitionTestSupport()); - this.applicationContext.registerBean("pkSelector", PartitionTestSupport.class, () -> new PartitionTestSupport()); - producerProperties.getExtension().setAutoBindDlq(true); - producerProperties.setPartitionKeyExtractorName("pkExtractor"); - producerProperties.setPartitionSelectorName("pkSelector"); - producerProperties.setPartitionCount(2); - BindingProperties bindingProperties = createProducerBindingProperties( - producerProperties); - DirectChannel output = createBindableChannel("output", bindingProperties); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("partDLQ.0", output, - producerProperties); - - final CountDownLatch latch0 = new CountDownLatch(1); - input0.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (latch0.getCount() <= 0) { - throw new RuntimeException("dlq"); - } - latch0.countDown(); - } - - }); - - final CountDownLatch latch1 = new CountDownLatch(1); - input1.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (latch1.getCount() <= 0) { - throw new RuntimeException("dlq"); - } - latch1.countDown(); - } - - }); - - output.send(new GenericMessage<>(1)); - assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); - - output.send(new GenericMessage<>(0)); - assertThat(latch0.await(10, TimeUnit.SECONDS)).isTrue(); - - output.send(new GenericMessage<>(1)); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.setReceiveTimeout(10000); - - String streamDLQName = "bindertest.partDLQ.0.dlqPartGrp.dlq"; - - org.springframework.amqp.core.Message received = template.receive(streamDLQName); - assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedRoutingKey()) - .isEqualTo("bindertest.partDLQ.0.dlqPartGrp-1"); - assertThat(received.getMessageProperties().getHeaders()) - .doesNotContainKey(BinderHeaders.PARTITION_HEADER); - - output.send(new GenericMessage<>(0)); - received = template.receive(streamDLQName); - assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedRoutingKey()) - .isEqualTo("bindertest.partDLQ.0.dlqPartGrp-0"); - assertThat(received.getMessageProperties().getHeaders()) - .doesNotContainKey(BinderHeaders.PARTITION_HEADER); - - input0Binding.unbind(); - input1Binding.unbind(); - defaultConsumerBinding1.unbind(); - defaultConsumerBinding2.unbind(); - outputBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - @Disabled - public void testAutoBindDLQPartitionedConsumerFirstWithRepublishNoRetry(TestInfo testInfo) - throws Exception { - testAutoBindDLQPartionedConsumerFirstWithRepublishGuts(false, testInfo); - } - - @Test - public void testAutoBindDLQPartitionedConsumerFirstWithRepublishWithRetry(TestInfo testInfo) - throws Exception { - testAutoBindDLQPartionedConsumerFirstWithRepublishGuts(true, testInfo); - } - - private void testAutoBindDLQPartionedConsumerFirstWithRepublishGuts( - final boolean withRetry, TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.getExtension().setPrefix("bindertest."); - properties.getExtension().setAutoBindDlq(true); - properties.getExtension().setRepublishToDlq(true); - properties.getExtension() - .setRepublishDeliveyMode(MessageDeliveryMode.NON_PERSISTENT); - properties.setMaxAttempts(withRetry ? 2 : 1); - properties.setPartitioned(true); - properties.setInstanceIndex(0); - properties.populateBindingName("blahblah"); - DirectChannel input0 = createBindableChannel("input", - createConsumerBindingProperties(properties)); - input0.setBeanName("test.input0DLQ"); - Binding input0Binding = binder.bindConsumer("partPubDLQ.0", - "dlqPartGrp", input0, properties); - Binding defaultConsumerBinding1 = binder - .bindConsumer("partPubDLQ.0", "default", new QueueChannel(), properties); - properties.setInstanceIndex(1); - DirectChannel input1 = createBindableChannel("input1", - createConsumerBindingProperties(properties)); - input1.setBeanName("test.input1DLQ"); - Binding input1Binding = binder.bindConsumer("partPubDLQ.0", - "dlqPartGrp", input1, properties); - Binding defaultConsumerBinding2 = binder - .bindConsumer("partPubDLQ.0", "default", new QueueChannel(), properties); - - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setPrefix("bindertest."); - producerProperties.getExtension().setAutoBindDlq(true); - this.applicationContext.registerBean("pkExtractor", PartitionTestSupport.class, () -> new PartitionTestSupport()); - this.applicationContext.registerBean("pkSelector", PartitionTestSupport.class, () -> new PartitionTestSupport()); - producerProperties.setPartitionKeyExtractorName("pkExtractor"); - producerProperties.setPartitionSelectorName("pkSelector"); - producerProperties.setPartitionCount(2); - BindingProperties bindingProperties = createProducerBindingProperties( - producerProperties); - DirectChannel output = createBindableChannel("output", bindingProperties); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("partPubDLQ.0", - output, producerProperties); - - final CountDownLatch latch0 = new CountDownLatch(1); - input0.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (latch0.getCount() <= 0) { - throw new RuntimeException("dlq"); - } - latch0.countDown(); - } - - }); - - final CountDownLatch latch1 = new CountDownLatch(1); - input1.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (latch1.getCount() <= 0) { - throw new RuntimeException("dlq"); - } - latch1.countDown(); - } - - }); - ApplicationContext context = TestUtils.getPropertyValue(binder.getBinder(), - "applicationContext", ApplicationContext.class); - SubscribableChannel boundErrorChannel = context.getBean(testBinder.getBinder().getBinderIdentity() + ".blahblah.errors", SubscribableChannel.class); - SubscribableChannel globalErrorChannel = context.getBean("errorChannel", - SubscribableChannel.class); - final AtomicReference> boundErrorChannelMessage = new AtomicReference<>(); - final AtomicReference> globalErrorChannelMessage = new AtomicReference<>(); - final AtomicBoolean hasRecovererInCallStack = new AtomicBoolean(!withRetry); - boundErrorChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - boundErrorChannelMessage.set(message); - String stackTrace = Arrays - .toString(new RuntimeException().getStackTrace()); - hasRecovererInCallStack - .set(stackTrace.contains("ErrorMessageSendingRecoverer")); - } - - }); - globalErrorChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - globalErrorChannelMessage.set(message); - } - - }); - - output.send(new GenericMessage<>(1)); - assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); - - output.send(new GenericMessage<>(0)); - assertThat(latch0.await(10, TimeUnit.SECONDS)).isTrue(); - - output.send(new GenericMessage<>(1)); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.setReceiveTimeout(10000); - - String streamDLQName = "bindertest.partPubDLQ.0.dlqPartGrp.dlq"; - - org.springframework.amqp.core.Message received = template.receive(streamDLQName); - assertThat(received).isNotNull(); - assertThat( - received.getMessageProperties().getHeaders().get("x-original-routingKey")) - .isEqualTo("partPubDLQ.0-1"); - assertThat(received.getMessageProperties().getHeaders()) - .doesNotContainKey(BinderHeaders.PARTITION_HEADER); - assertThat(received.getMessageProperties().getReceivedDeliveryMode()) - .isEqualTo(MessageDeliveryMode.NON_PERSISTENT); - - output.send(new GenericMessage<>(0)); - received = template.receive(streamDLQName); - assertThat(received).isNotNull(); - assertThat( - received.getMessageProperties().getHeaders().get("x-original-routingKey")) - .isEqualTo("partPubDLQ.0-0"); - assertThat(received.getMessageProperties().getHeaders()) - .doesNotContainKey(BinderHeaders.PARTITION_HEADER); - - // verify we got a message on the dedicated error channel and the global (via - // bridge) - // assertThat(boundErrorChannelMessage.get()).isNotNull(); - // assertThat(globalErrorChannelMessage.get()).isNotNull(); -// assertThat(hasRecovererInCallStack.get()).isEqualTo(withRetry); - - input0Binding.unbind(); - input1Binding.unbind(); - defaultConsumerBinding1.unbind(); - defaultConsumerBinding2.unbind(); - outputBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testAutoBindDLQPartitionedProducerFirst(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties properties = createProducerProperties(testInfo); - - properties.getExtension().setPrefix("bindertest."); - properties.getExtension().setAutoBindDlq(true); - properties.setRequiredGroups("dlqPartGrp"); - this.applicationContext.registerBean("pkExtractor", PartitionTestSupport.class, () -> new PartitionTestSupport()); - properties.setPartitionKeyExtractorName("pkExtractor"); - properties.setPartitionSelectorName("pkExtractor"); - properties.setPartitionCount(2); - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(properties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer("partDLQ.1", output, - properties); - - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix("bindertest."); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.setMaxAttempts(1); // disable retry - consumerProperties.setPartitioned(true); - consumerProperties.setInstanceIndex(0); - DirectChannel input0 = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - input0.setBeanName("test.input0DLQ"); - Binding input0Binding = binder.bindConsumer("partDLQ.1", - "dlqPartGrp", input0, consumerProperties); - Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.1", - "defaultConsumer", new QueueChannel(), consumerProperties); - consumerProperties.setInstanceIndex(1); - DirectChannel input1 = createBindableChannel("input1", - createConsumerBindingProperties(consumerProperties)); - input1.setBeanName("test.input1DLQ"); - Binding input1Binding = binder.bindConsumer("partDLQ.1", - "dlqPartGrp", input1, consumerProperties); - Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.1", - "defaultConsumer", new QueueChannel(), consumerProperties); - - final CountDownLatch latch0 = new CountDownLatch(1); - input0.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (latch0.getCount() <= 0) { - throw new RuntimeException("dlq"); - } - latch0.countDown(); - } - - }); - - final CountDownLatch latch1 = new CountDownLatch(1); - input1.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (latch1.getCount() <= 0) { - throw new RuntimeException("dlq"); - } - latch1.countDown(); - } - - }); - - output.send(new GenericMessage(1)); - assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); - - output.send(new GenericMessage(0)); - assertThat(latch0.await(10, TimeUnit.SECONDS)).isTrue(); - - output.send(new GenericMessage(1)); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.setReceiveTimeout(10000); - - String streamDLQName = "bindertest.partDLQ.1.dlqPartGrp.dlq"; - - org.springframework.amqp.core.Message received = template.receive(streamDLQName); - assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedRoutingKey()) - .isEqualTo("bindertest.partDLQ.1.dlqPartGrp-1"); - assertThat(received.getMessageProperties().getHeaders()) - .doesNotContainKey(BinderHeaders.PARTITION_HEADER); - assertThat(received.getMessageProperties().getReceivedDeliveryMode()) - .isEqualTo(MessageDeliveryMode.PERSISTENT); - - output.send(new GenericMessage(0)); - received = template.receive(streamDLQName); - assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedRoutingKey()) - .isEqualTo("bindertest.partDLQ.1.dlqPartGrp-0"); - assertThat(received.getMessageProperties().getHeaders()) - .doesNotContainKey(BinderHeaders.PARTITION_HEADER); - - input0Binding.unbind(); - input1Binding.unbind(); - defaultConsumerBinding1.unbind(); - defaultConsumerBinding2.unbind(); - outputBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testAutoBindDLQwithRepublish() throws Exception { - this.maxStackTraceSize = RabbitUtils - .getMaxFrame(rabbitTestSupport.getResource()) - 20_000; - assertThat(this.maxStackTraceSize).isGreaterThan(0); - - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setRepublishToDlq(true); - consumerProperties.setMaxAttempts(1); // disable retry - consumerProperties.getExtension().setDurableSubscription(true); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - moduleInputChannel.setBeanName("dlqPubTest"); - RuntimeException exception = bigCause( - new RuntimeException(BIG_EXCEPTION_MESSAGE)); - assertThat(getStackTraceAsString(exception).length()) - .isGreaterThan(this.maxStackTraceSize); - AtomicBoolean dontRepublish = new AtomicBoolean(); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (dontRepublish.get()) { - throw new ImmediateAcknowledgeAmqpException("testDontRepublish"); - } - throw exception; - } - - }); - consumerProperties.setMultiplex(true); - Binding consumerBinding = binder.bindConsumer( - "foo.dlqpubtest,foo.dlqpubtest2", "foo", moduleInputChannel, - consumerProperties); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtest.foo", "foo"); - - template.setReceiveTimeout(10_000); - org.springframework.amqp.core.Message deadLetter = template - .receive(TEST_PREFIX + "foo.dlqpubtest.foo.dlq"); - assertThat(deadLetter).isNotNull(); - assertThat(new String(deadLetter.getBody())).isEqualTo("foo"); - assertThat(deadLetter.getMessageProperties().getHeaders()) - .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); - assertThat(((LongString) deadLetter.getMessageProperties().getHeaders() - .get(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)).length()) - .isEqualTo(this.maxStackTraceSize); - - template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtest2.foo", "bar"); - - deadLetter = template.receive(TEST_PREFIX + "foo.dlqpubtest2.foo.dlq"); - assertThat(deadLetter).isNotNull(); - assertThat(new String(deadLetter.getBody())).isEqualTo("bar"); - assertThat(deadLetter.getMessageProperties().getHeaders()) - .containsKey(("x-exception-stacktrace")); - - dontRepublish.set(true); - template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtest2.foo", "baz"); - template.setReceiveTimeout(500); - assertThat(template.receive(TEST_PREFIX + "foo.dlqpubtest2.foo.dlq")).isNull(); - - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testAutoBindDLQwithRepublishTx() throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setRepublishToDlq(true); - consumerProperties.setMaxAttempts(1); // disable retry - consumerProperties.getExtension().setDurableSubscription(true); - consumerProperties.getExtension().setTransacted(true); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - moduleInputChannel.setBeanName("dlqPubTestTx"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - throw new RuntimeException("test"); - } - - }); - Binding consumerBinding = binder.bindConsumer( - "foo.dlqpubtestTx", "foo", moduleInputChannel, consumerProperties); - - RabbitTemplate template = new RabbitTemplate(this.rabbitTestSupport.getResource()); - template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtestTx.foo", "foo"); - - template.setReceiveTimeout(10_000); - org.springframework.amqp.core.Message deadLetter = template - .receive(TEST_PREFIX + "foo.dlqpubtestTx.foo.dlq"); - assertThat(deadLetter).isNotNull(); - assertThat(deadLetter.getBody()).isEqualTo("foo".getBytes()); - assertThat(deadLetter.getMessageProperties().getHeaders()) - .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); - List errorHandler = (List) TestUtils.getPropertyValue(consumerBinding, - "lifecycle.errorChannel.dispatcher.handlers", Set.class).stream() - .filter(handler -> !handler.getClass().equals(BridgeHandler.class)) - .collect(Collectors.toList()); - assertThat(errorHandler).hasSize(1); - assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "template.transactional", Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) - .isEqualTo(ConfirmType.NONE); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testAutoBindDLQwithRepublishSimpleConfirms() throws Exception { - CachingConnectionFactory ccf = this.rabbitTestSupport.getResource(); - ccf.setPublisherReturns(true); - ccf.setPublisherConfirmType(ConfirmType.SIMPLE); - ccf.resetConnection(); - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setRepublishToDlq(true); - consumerProperties.setMaxAttempts(1); // disable retry - consumerProperties.getExtension().setDurableSubscription(true); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - moduleInputChannel.setBeanName("dlqPubtestSimple"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - throw new RuntimeException("test"); - } - - }); - Binding consumerBinding = binder.bindConsumer( - "foo.dlqpubtestSimple", "foo", moduleInputChannel, consumerProperties); - - RabbitTemplate template = new RabbitTemplate(this.rabbitTestSupport.getResource()); - template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtestSimple.foo", "foo"); - - template.setReceiveTimeout(10_000); - org.springframework.amqp.core.Message deadLetter = template - .receive(TEST_PREFIX + "foo.dlqpubtestSimple.foo.dlq"); - assertThat(deadLetter).isNotNull(); - assertThat(deadLetter.getBody()).isEqualTo("foo".getBytes()); - assertThat(deadLetter.getMessageProperties().getHeaders()) - .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); - List errorHandler = (List) TestUtils.getPropertyValue(consumerBinding, - "lifecycle.errorChannel.dispatcher.handlers", Set.class).stream() - .filter(handler -> !handler.getClass().equals(BridgeHandler.class)) - .collect(Collectors.toList()); - assertThat(errorHandler).hasSize(1); - assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) - .isEqualTo(ConfirmType.SIMPLE); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testAutoBindDLQwithRepublishCorrelatedConfirms() throws Exception { - CachingConnectionFactory ccf = this.rabbitTestSupport.getResource(); - ccf.setPublisherReturns(true); - ccf.setPublisherConfirmType(ConfirmType.CORRELATED); - ccf.resetConnection(); - RabbitTestBinder binder = getBinder(); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.getExtension().setPrefix(TEST_PREFIX); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setRepublishToDlq(true); - consumerProperties.setMaxAttempts(1); // disable retry - consumerProperties.getExtension().setDurableSubscription(true); - DirectChannel moduleInputChannel = createBindableChannel("input", - createConsumerBindingProperties(consumerProperties)); - moduleInputChannel.setBeanName("dlqPubtestCorrelated"); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - throw new RuntimeException("test"); - } - - }); - Binding consumerBinding = binder.bindConsumer( - "foo.dlqpubtestCorrelated", "foo", moduleInputChannel, consumerProperties); - - RabbitTemplate template = new RabbitTemplate(this.rabbitTestSupport.getResource()); - template.convertAndSend("", TEST_PREFIX + "foo.dlqpubtestCorrelated.foo", "foo"); - - template.setReceiveTimeout(10_000); - org.springframework.amqp.core.Message deadLetter = template - .receive(TEST_PREFIX + "foo.dlqpubtestCorrelated.foo.dlq"); - assertThat(deadLetter).isNotNull(); - assertThat(deadLetter.getBody()).isEqualTo("foo".getBytes()); - assertThat(deadLetter.getMessageProperties().getHeaders()) - .containsKey((RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE)); - List errorHandler = (List) TestUtils.getPropertyValue(consumerBinding, - "lifecycle.errorChannel.dispatcher.handlers", Set.class).stream() - .filter(handler -> !handler.getClass().equals(BridgeHandler.class)) - .collect(Collectors.toList()); - assertThat(errorHandler).hasSize(1); - assertThat(TestUtils.getPropertyValue(errorHandler.get(0), "confirmType", ConfirmType.class)) - .isEqualTo(ConfirmType.CORRELATED); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testBatchingAndCompression(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension() - .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - producerProperties.getExtension().setBatchingEnabled(true); - producerProperties.getExtension().setBatchSize(2); - producerProperties.getExtension().setBatchBufferLimit(100000); - producerProperties.getExtension().setBatchTimeout(30000); - producerProperties.getExtension().setCompress(true); - producerProperties.setRequiredGroups("default"); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("batchingProducer"); - Binding producerBinding = binder.bindProducer("batching.0", - output, producerProperties); - - Log logger = spy(TestUtils.getPropertyValue(binder, - "binder.compressingPostProcessor.logger", Log.class)); - new DirectFieldAccessor( - TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor")) - .setPropertyValue("logger", logger); - when(logger.isTraceEnabled()).thenReturn(true); - - assertThat(TestUtils.getPropertyValue(binder, - "binder.compressingPostProcessor.level")).isEqualTo(Deflater.BEST_SPEED); - - output.send(new GenericMessage<>("foo".getBytes())); - output.send(new GenericMessage<>("bar".getBytes())); - - Object out = spyOn("batching.0.default").receive(false); - assertThat(out).isInstanceOf(byte[].class); - assertThat(new String((byte[]) out)) - .isEqualTo("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); - verify(logger).trace(captor.capture()); - assertThat(captor.getValue().toString()).contains(("Compressed 14 to ")); - - QueueChannel input = new QueueChannel(); - input.setBeanName("batchingConsumer"); - Binding consumerBinding = binder.bindConsumer("batching.0", - "test", input, createConsumerProperties()); - - output.send(new GenericMessage<>("foo".getBytes())); - output.send(new GenericMessage<>("bar".getBytes())); - - Message in = (Message) input.receive(10000); - assertThat(in).isNotNull(); - assertThat(new String(in.getPayload())).isEqualTo("foo"); - in = (Message) input.receive(10000); - assertThat(in).isNotNull(); - assertThat(new String(in.getPayload())).isEqualTo("bar"); - assertThat(in.getHeaders().get(AmqpHeaders.DELIVERY_MODE)).isNull(); - - producerBinding.unbind(); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testProducerBatching(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension() - .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - producerProperties.getExtension().setBatchingEnabled(true); - producerProperties.getExtension().setBatchSize(2); - producerProperties.getExtension().setBatchBufferLimit(100000); - producerProperties.getExtension().setBatchTimeout(30000); - producerProperties.getExtension().setCompress(true); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("producerBatchingProducer"); - Binding producerBinding = binder.bindProducer("p.batching.0", - output, producerProperties); - - QueueChannel input = new QueueChannel(); - input.setBeanName("producerBatchingConsumer"); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setBatchMode(true); - consumerProperties.getExtension().setBatchSize(2); - Binding consumerBinding = binder.bindConsumer("p.batching.0", - "producerBatching", input, consumerProperties); - output.send(new GenericMessage<>("foo".getBytes())); - output.send(new GenericMessage<>("bar".getBytes())); - - - Message in = input.receive(10000); - assertThat(in).isNotNull(); - assertThat(in.getPayload()).isInstanceOf(List.class); - List payload = (List) in.getPayload(); - assertThat(payload).hasSize(2); - assertThat(payload.get(0)).isEqualTo("foo".getBytes()); - assertThat(payload.get(1)).isEqualTo("bar".getBytes()); - - producerBinding.unbind(); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @SuppressWarnings("unchecked") - @Test - public void testConsumerBatching(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension() - .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("consumerBatching.Producer"); - Binding producerBinding = binder.bindProducer("c.batching.0", - output, producerProperties); - - QueueChannel input = new QueueChannel(); - input.setBeanName("batchingConsumer"); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setBatchMode(true); - consumerProperties.getExtension().setBatchSize(2); - consumerProperties.getExtension().setEnableBatching(true); - Binding consumerBinding = binder.bindConsumer("c.batching.0", - "consumerBatching", input, consumerProperties); - output.send(new GenericMessage<>("foo".getBytes())); - output.send(new GenericMessage<>("bar".getBytes())); - - - Message in = input.receive(10000); - assertThat(in).isNotNull(); - assertThat(in.getPayload()).isInstanceOf(List.class); - List payload = (List) in.getPayload(); - assertThat(payload).hasSize(2); - assertThat(payload.get(0)).isEqualTo("foo".getBytes()); - assertThat(payload.get(1)).isEqualTo("bar".getBytes()); - - producerBinding.unbind(); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testErrorMessageHandlerForBatchModeDLQRepublish(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension() - .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("consumerBatching.Producer"); - Binding producerBinding = binder.bindProducer("c.batching.0", - output, producerProperties); - - DirectChannel moduleInputChannel = createBindableChannel("input", - new BindingProperties()); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - consumerProperties.setBatchMode(true); - consumerProperties.getExtension().setBatchSize(2); - consumerProperties.getExtension().setEnableBatching(true); - consumerProperties.getExtension().setRepublishToDlq(true); - consumerProperties.getExtension().setAutoBindDlq(true); - consumerProperties.getExtension().setDeadLetterExchange("dlqTest"); - Binding consumerBinding = binder.bindConsumer("c.batching.0", - "consumerBatching", moduleInputChannel, consumerProperties); - - List expectedMessages = List.of("message1","message2"); - - output.send(new GenericMessage<>(expectedMessages.get(0).getBytes())); - output.send(new GenericMessage<>(expectedMessages.get(1).getBytes())); - - moduleInputChannel.subscribe(message -> { - throw new RuntimeException("Exception"); - }); - - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - long now = Instant.now().toEpochMilli(); - List deadLetterMessages = new ArrayList<>(); - while(deadLetterMessages.size() < 2 || (Instant.now().toEpochMilli() - now) > 60_000){ - deadLetterMessages.add(template - .receive("c.batching.0.consumerBatching.dlq", 10_000)); - } - - assertThat(new String(deadLetterMessages.get(0).getBody())).isIn(expectedMessages); - assertThat(new String(deadLetterMessages.get(0).getBody())).isIn(expectedMessages); - - producerBinding.unbind(); - consumerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @SuppressWarnings("unchecked") - @Test - public void testInternalHeadersNotPropagated(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension() - .setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("propagate.out"); - Binding producerBinding = binder.bindProducer("propagate.1", - output, producerProperties); - - QueueChannel input = new QueueChannel(); - input.setBeanName("propagate.in"); - ExtendedConsumerProperties consumerProperties = createConsumerProperties(); - Binding consumerBinding = binder.bindConsumer("propagate.0", - "propagate", input, consumerProperties); - RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.declareQueue(new Queue("propagate")); - admin.declareBinding(new org.springframework.amqp.core.Binding("propagate", DestinationType.QUEUE, - "propagate.1", "#", null)); - RabbitTemplate template = new RabbitTemplate(this.rabbitTestSupport.getResource()); - template.convertAndSend("propagate.0.propagate", "foo"); - output.send(input.receive(10_000)); - org.springframework.amqp.core.Message received = template.receive("propagate", 10_000); - assertThat(received).isNotNull(); - assertThat(received.getBody()).isEqualTo("foo".getBytes()); - Object header = received.getMessageProperties().getHeader(IntegrationMessageHeaderAccessor.SOURCE_DATA); - assertThat(header).isNull(); - header = received.getMessageProperties().getHeader(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT); - assertThat(header).isNull(); - - producerBinding.unbind(); - consumerBinding.unbind(); - admin.deleteQueue("propagate"); - - verifyAutoDeclareContextClear(binder); - } - - - /* - * Test late binding due to broker down; queues with and without DLQs, and partitioned - * queues. - */ - @Test - public void testLateBinding(TestInfo testInfo) throws Exception { - RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); - CachingConnectionFactory cf = new CachingConnectionFactory("localhost", - proxy.getPort()); - - RabbitMessageChannelBinder rabbitBinder = new RabbitMessageChannelBinder(cf, - new RabbitProperties(), new RabbitExchangeQueueProvisioner(cf)); - RabbitTestBinder binder = new RabbitTestBinder(cf, rabbitBinder); - - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setPrefix("latebinder."); - producerProperties.getExtension().setAutoBindDlq(true); - producerProperties.getExtension().setTransacted(true); - - MessageChannel moduleOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - Binding late0ProducerBinding = binder.bindProducer("late.0", - moduleOutputChannel, producerProperties); - - QueueChannel moduleInputChannel = new QueueChannel(); - ExtendedConsumerProperties rabbitConsumerProperties = createConsumerProperties(); - rabbitConsumerProperties.getExtension().setPrefix("latebinder."); - Binding late0ConsumerBinding = binder.bindConsumer("late.0", - "test", moduleInputChannel, rabbitConsumerProperties); - - producerProperties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("payload.equals('0') ? 0 : 1")); - producerProperties.setPartitionSelectorExpression( - spelExpressionParser.parseExpression("hashCode()")); - producerProperties.setPartitionCount(2); - - MessageChannel partOutputChannel = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - Binding partlate0ProducerBinding = binder - .bindProducer("partlate.0", partOutputChannel, producerProperties); - - QueueChannel partInputChannel0 = new QueueChannel(); - QueueChannel partInputChannel1 = new QueueChannel(); - - ExtendedConsumerProperties partLateConsumerProperties = createConsumerProperties(); - partLateConsumerProperties.getExtension().setPrefix("latebinder."); - partLateConsumerProperties.setPartitioned(true); - partLateConsumerProperties.setInstanceIndex(0); - Binding partlate0Consumer0Binding = binder.bindConsumer( - "partlate.0", "test", partInputChannel0, partLateConsumerProperties); - partLateConsumerProperties.setInstanceIndex(1); - Binding partlate0Consumer1Binding = binder.bindConsumer( - "partlate.0", "test", partInputChannel1, partLateConsumerProperties); - - ExtendedProducerProperties noDlqProducerProperties = createProducerProperties(testInfo); - noDlqProducerProperties.getExtension().setPrefix("latebinder."); - MessageChannel noDLQOutputChannel = createBindableChannel("output", - createProducerBindingProperties(noDlqProducerProperties)); - Binding noDlqProducerBinding = binder.bindProducer("lateNoDLQ.0", - noDLQOutputChannel, noDlqProducerProperties); - - QueueChannel noDLQInputChannel = new QueueChannel(); - ExtendedConsumerProperties noDlqConsumerProperties = createConsumerProperties(); - noDlqConsumerProperties.getExtension().setPrefix("latebinder."); - Binding noDlqConsumerBinding = binder.bindConsumer("lateNoDLQ.0", - "test", noDLQInputChannel, noDlqConsumerProperties); - - MessageChannel outputChannel = createBindableChannel("output", - createProducerBindingProperties(noDlqProducerProperties)); - Binding pubSubProducerBinding = binder.bindProducer("latePubSub", - outputChannel, noDlqProducerProperties); - QueueChannel pubSubInputChannel = new QueueChannel(); - noDlqConsumerProperties.getExtension().setDurableSubscription(false); - Binding nonDurableConsumerBinding = binder.bindConsumer( - "latePubSub", "lategroup", pubSubInputChannel, noDlqConsumerProperties); - QueueChannel durablePubSubInputChannel = new QueueChannel(); - noDlqConsumerProperties.getExtension().setDurableSubscription(true); - Binding durableConsumerBinding = binder.bindConsumer("latePubSub", - "lateDurableGroup", durablePubSubInputChannel, noDlqConsumerProperties); - - proxy.start(RABBITMQ.getAmqpPort()); - - moduleOutputChannel.send(MessageBuilder.withPayload("foo") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - Message message = moduleInputChannel.receive(10000); - assertThat(message).isNotNull(); - assertThat(message.getPayload()).isNotNull(); - - noDLQOutputChannel.send(MessageBuilder.withPayload("bar") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - message = noDLQInputChannel.receive(10000); - assertThat(message); - assertThat(message.getPayload()).isEqualTo("bar".getBytes()); - - outputChannel.send(MessageBuilder.withPayload("baz") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - message = pubSubInputChannel.receive(10000); - assertThat(message); - assertThat(message.getPayload()).isEqualTo("baz".getBytes()); - message = durablePubSubInputChannel.receive(10000); - assertThat(message).isNotNull(); - assertThat(message.getPayload()).isEqualTo("baz".getBytes()); - - partOutputChannel.send(MessageBuilder.withPayload("0") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - partOutputChannel.send(MessageBuilder.withPayload("1") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - message = partInputChannel0.receive(10000); - assertThat(message).isNotNull(); - assertThat(message.getPayload()).isEqualTo("0".getBytes()); - message = partInputChannel1.receive(10000); - assertThat(message).isNotNull(); - assertThat(message.getPayload()).isEqualTo("1".getBytes()); - - late0ProducerBinding.unbind(); - late0ConsumerBinding.unbind(); - partlate0ProducerBinding.unbind(); - partlate0Consumer0Binding.unbind(); - partlate0Consumer1Binding.unbind(); - noDlqProducerBinding.unbind(); - noDlqConsumerBinding.unbind(); - pubSubProducerBinding.unbind(); - nonDurableConsumerBinding.unbind(); - durableConsumerBinding.unbind(); - - binder.cleanup(); - - proxy.stop(); - cf.destroy(); - - this.rabbitTestSupport.getResource().destroy(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testBadUserDeclarationsFatal() throws Exception { - RabbitTestBinder binder = getBinder(); - ConfigurableApplicationContext context = binder.getApplicationContext(); - ConfigurableListableBeanFactory bf = context.getBeanFactory(); - bf.registerSingleton("testBadUserDeclarationsFatal", - new Queue("testBadUserDeclarationsFatal", false)); - bf.registerSingleton("binder", binder); - RabbitExchangeQueueProvisioner provisioner = TestUtils.getPropertyValue(binder, - "binder.provisioningProvider", RabbitExchangeQueueProvisioner.class); - bf.initializeBean(provisioner, "provisioner"); - bf.registerSingleton("provisioner", provisioner); - context.addApplicationListener(provisioner); - RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.declareQueue(new Queue("testBadUserDeclarationsFatal")); - // reset the connection and configure the "user" admin to auto declare queues... - rabbitTestSupport.getResource().resetConnection(); - bf.initializeBean(admin, "rabbitAdmin"); - bf.registerSingleton("rabbitAdmin", admin); - admin.afterPropertiesSet(); - // the mis-configured queue should be fatal - Binding binding = null; - try { - binding = binder.bindConsumer("input", "baddecls", - this.createBindableChannel("input", new BindingProperties()), - createConsumerProperties()); - fail("Expected exception"); - } - catch (BinderException e) { - assertThat(e.getCause()).isInstanceOf(AmqpIOException.class); - } - finally { - admin.deleteQueue("testBadUserDeclarationsFatal"); - if (binding != null) { - binding.unbind(); - } - } - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testRoutingKeyExpression(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setRoutingKeyExpression( - spelExpressionParser.parseExpression("payload.field")); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("rkeProducer"); - Binding producerBinding = binder.bindProducer("rke", output, - producerProperties); - - RabbitAdmin admin = new RabbitAdmin(this.rabbitTestSupport.getResource()); - Queue queue = new AnonymousQueue(); - TopicExchange exchange = new TopicExchange("rke"); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) - .to(exchange).with("rkeTest"); - admin.declareQueue(queue); - admin.declareBinding(binding); - - output.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - assertThat(message.getHeaders() - .get(RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER)) - .isEqualTo("rkeTest"); - return message; - } - - }); - - output.send(new GenericMessage<>(new Pojo("rkeTest"))); - - Object out = spyOn(queue.getName()).receive(false); - assertThat(out).isInstanceOf(byte[].class); - assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) - .isEqualTo("{\"field\":\"rkeTest\"}"); - - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testRoutingKey(TestInfo testInfo) throws Exception { - String routingKey = "static.key"; - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setRoutingKey(routingKey); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("rkeProducer"); - Binding producerBinding = binder.bindProducer("rke", output, - producerProperties); - - RabbitAdmin admin = new RabbitAdmin(this.rabbitTestSupport.getResource()); - Queue queue = new AnonymousQueue(); - DirectExchange exchange = new DirectExchange("rke"); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) - .to(exchange).with(routingKey); - admin.declareQueue(queue); - admin.declareBinding(binding); - - output.send(new GenericMessage<>(new Pojo("rkeTest"))); - - Object out = spyOn(queue.getName()).receive(false); - assertThat(out).isInstanceOf(byte[].class); - assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) - .isEqualTo("{\"field\":\"rkeTest\"}"); - - - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testRoutingKeyExpressionPartitionedAndDelay(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setRoutingKeyExpression( - spelExpressionParser.parseExpression("#root.getPayload().field")); - // requires delayed message exchange plugin; tested locally - // producerProperties.getExtension().setDelayedExchange(true); - producerProperties.getExtension() - .setDelayExpression(spelExpressionParser.parseExpression("1000")); - producerProperties.setPartitionKeyExpression(new ValueExpression<>(0)); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("rkeProducer"); - Binding producerBinding = binder.bindProducer("rkep", output, - producerProperties); - - RabbitAdmin admin = new RabbitAdmin(this.rabbitTestSupport.getResource()); - Queue queue = new AnonymousQueue(); - TopicExchange exchange = new TopicExchange("rkep"); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue) - .to(exchange).with("rkepTest-0"); - admin.declareQueue(queue); - admin.declareBinding(binding); - - output.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - assertThat(message.getHeaders() - .get(RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER)) - .isEqualTo("rkepTest"); - assertThat(message.getHeaders() - .get(RabbitExpressionEvaluatingInterceptor.DELAY_HEADER)) - .isEqualTo(1000); - return message; - } - - }); - - output.send(new GenericMessage<>(new Pojo("rkepTest"))); - - Object out = spyOn(queue.getName()).receive(false); - assertThat(out).isInstanceOf(byte[].class); - assertThat(new String((byte[]) out, StandardCharsets.UTF_8)) - .isEqualTo("{\"field\":\"rkepTest\"}"); - - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testPolledConsumer() throws Exception { - RabbitTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - Binding> binding = binder.bindPollableConsumer( - "pollable", "group", inboundBindTarget, createConsumerProperties()); - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("pollable.group", "testPollable"); - boolean polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable"); - }); - int n = 0; - while (n++ < 100 && !polled) { - polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable"); - }); - } - assertThat(polled).isTrue(); - binding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testPolledConsumerRequeue() throws Exception { - RabbitTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties properties = createConsumerProperties(); - Binding> binding = binder.bindPollableConsumer( - "pollableRequeue", "group", inboundBindTarget, properties); - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("pollableRequeue.group", "testPollable"); - try { - boolean polled = false; - int n = 0; - while (n++ < 100 && !polled) { - polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable"); - throw new RequeueCurrentMessageException(); - }); - } - } - catch (MessageHandlingException e) { - assertThat(e.getCause()).isInstanceOf(RequeueCurrentMessageException.class); - } - boolean polled = inboundBindTarget.poll(m -> { - assertThat(m.getPayload()).isEqualTo("testPollable"); - }); - assertThat(polled).isTrue(); - binding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testPolledConsumerWithDlq() throws Exception { - RabbitTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - properties.getExtension().setAutoBindDlq(true); - Binding> binding = binder.bindPollableConsumer( - "pollableDlq", "group", inboundBindTarget, properties); - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("pollableDlq.group", "testPollable"); - try { - int n = 0; - while (n++ < 100) { - inboundBindTarget.poll(m -> { - throw new RuntimeException("test DLQ"); - }); - Thread.sleep(100); - } - } - catch (MessageHandlingException e) { - assertThat( - e.getCause().getCause().getCause().getCause().getCause().getMessage()) - .isEqualTo("test DLQ"); - } - org.springframework.amqp.core.Message deadLetter = template - .receive("pollableDlq.group.dlq", 10_000); - assertThat(deadLetter).isNotNull(); - binding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testPolledConsumerWithDlqNoRetry() throws Exception { - RabbitTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.setMaxAttempts(1); - // properties.getExtension().setRequeueRejected(true); // loops, correctly - properties.getExtension().setAutoBindDlq(true); - Binding> binding = binder.bindPollableConsumer( - "pollableDlqNoRetry", "group", inboundBindTarget, properties); - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("pollableDlqNoRetry.group", "testPollable"); - try { - int n = 0; - while (n++ < 100) { - inboundBindTarget.poll(m -> { - throw new RuntimeException("test DLQ"); - }); - Thread.sleep(100); - } - } - catch (MessageHandlingException e) { - assertThat(e.getCause().getMessage()).isEqualTo("test DLQ"); - } - org.springframework.amqp.core.Message deadLetter = template - .receive("pollableDlqNoRetry.group.dlq", 10_000); - assertThat(deadLetter).isNotNull(); - binding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - @Test - public void testPolledConsumerWithDlqRePub() throws Exception { - RabbitTestBinder binder = getBinder(); - PollableSource inboundBindTarget = new DefaultPollableMessageSource( - this.messageConverter); - ExtendedConsumerProperties properties = createConsumerProperties(); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - properties.getExtension().setAutoBindDlq(true); - properties.getExtension().setRepublishToDlq(true); - Binding> binding = binder.bindPollableConsumer( - "pollableDlqRePub", "group", inboundBindTarget, properties); - RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.convertAndSend("pollableDlqRePub.group", "testPollable"); - boolean polled = false; - int n = 0; - while (n++ < 100 && !polled) { - Thread.sleep(100); - polled = inboundBindTarget.poll(m -> { - throw new RuntimeException("test DLQ"); - }); - } - assertThat(polled).isTrue(); - org.springframework.amqp.core.Message deadLetter = template - .receive("pollableDlqRePub.group.dlq", 10_000); - assertThat(deadLetter).isNotNull(); - binding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - @Test - public void testCustomBatchingStrategy(TestInfo testInfo) throws Exception { - RabbitTestBinder binder = getBinder(); - ExtendedProducerProperties producerProperties = createProducerProperties(testInfo); - producerProperties.getExtension().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT); - producerProperties.getExtension().setBatchingEnabled(true); - producerProperties.getExtension().setBatchingStrategyBeanName("testCustomBatchingStrategy"); - producerProperties.setRequiredGroups("default"); - - ConfigurableListableBeanFactory beanFactory = binder.getApplicationContext().getBeanFactory(); - beanFactory.registerSingleton("testCustomBatchingStrategy", new TestBatchingStrategy()); - - DirectChannel output = createBindableChannel("output", createProducerBindingProperties(producerProperties)); - output.setBeanName("batchingProducer"); - Binding producerBinding = binder.bindProducer("batching.0", output, producerProperties); - - Log logger = spy(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor.logger", Log.class)); - new DirectFieldAccessor(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor")) - .setPropertyValue("logger", logger); - when(logger.isTraceEnabled()).thenReturn(true); - - assertThat(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor.level")) - .isEqualTo(Deflater.BEST_SPEED); - - output.send(new GenericMessage<>("0".getBytes())); - output.send(new GenericMessage<>("1".getBytes())); - output.send(new GenericMessage<>("2".getBytes())); - output.send(new GenericMessage<>("3".getBytes())); - output.send(new GenericMessage<>("4".getBytes())); - - Object out = spyOn("batching.0.default").receive(false); - assertThat(out).isInstanceOf(byte[].class); - assertThat(new String((byte[]) out)).isEqualTo("0\u0000\n1\u0000\n2\u0000\n3\u0000\n4\u0000\n"); - - producerBinding.unbind(); - - verifyAutoDeclareContextClear(binder); - } - - - private SimpleMessageListenerContainer verifyContainer(Lifecycle endpoint) { - SimpleMessageListenerContainer container; - RetryTemplate retry; - container = TestUtils.getPropertyValue(endpoint, "messageListenerContainer", - SimpleMessageListenerContainer.class); - assertThat(container.getAcknowledgeMode()).isEqualTo(AcknowledgeMode.NONE); - assertThat(container.getQueueNames()[0]).startsWith("foo.props.0"); - assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)) - .isFalse(); - assertThat(TestUtils.getPropertyValue(container, "concurrentConsumers")) - .isEqualTo(2); - assertThat(TestUtils.getPropertyValue(container, "maxConcurrentConsumers")) - .isEqualTo(3); - assertThat(TestUtils.getPropertyValue(container, "defaultRequeueRejected", - Boolean.class)).isFalse(); - assertThat(TestUtils.getPropertyValue(container, "prefetchCount")).isEqualTo(20); - assertThat(TestUtils.getPropertyValue(container, "batchSize")).isEqualTo(10); - retry = TestUtils.getPropertyValue(endpoint, "retryTemplate", - RetryTemplate.class); - assertThat(TestUtils.getPropertyValue(retry, "retryPolicy.maxAttempts")) - .isEqualTo(23); - assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.initialInterval")) - .isEqualTo(2000L); - assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.maxInterval")) - .isEqualTo(20000L); - assertThat(TestUtils.getPropertyValue(retry, "backOffPolicy.multiplier")) - .isEqualTo(5.0); - - List requestMatchers = TestUtils.getPropertyValue(endpoint, - "headerMapper.requestHeaderMatcher.matchers", List.class); - assertThat(requestMatchers).hasSize(1); - assertThat(TestUtils.getPropertyValue(requestMatchers.get(0), "pattern")) - .isEqualTo("foo"); - - return container; - } - - private void verifyFooRequestProducer(Lifecycle endpoint) { - List requestMatchers = TestUtils.getPropertyValue(endpoint, - "headerMapper.requestHeaderMatcher.matchers", List.class); - assertThat(requestMatchers).hasSize(5); - assertThat(TestUtils.getPropertyValue(requestMatchers.get(4), "pattern")) - .isEqualTo("foo"); - } - - @Override - protected String getEndpointRouting(Object endpoint) { - return TestUtils - .getPropertyValue(endpoint, "routingKeyExpression", SpelExpression.class) - .getExpressionString(); - } - - @Override - protected String getExpectedRoutingBaseDestination(String name, String group) { - return name; - } - - @Override - protected String getClassUnderTestName() { - return CLASS_UNDER_TEST_NAME; - } - - @Override - protected void checkRkExpressionForPartitionedModuleSpEL(Object endpoint) { - assertThat(getEndpointRouting(endpoint)) - .contains(getExpectedRoutingBaseDestination("'part.0'", "test") - + " + '-' + headers['" + BinderHeaders.PARTITION_HEADER + "']"); - } - - @Override - public Spy spyOn(final String queue) { - final RabbitTemplate template = new RabbitTemplate( - this.rabbitTestSupport.getResource()); - template.setAfterReceivePostProcessors( - new DelegatingDecompressingPostProcessor()); - return new Spy() { - - @Override - public Object receive(boolean expectNull) throws Exception { - if (expectNull) { - Thread.sleep(50); - return template.receiveAndConvert( - new RabbitConsumerProperties().getPrefix() + queue); - } - Object bar = null; - int n = 0; - while (n++ < 100 && bar == null) { - bar = template.receiveAndConvert( - new RabbitConsumerProperties().getPrefix() + queue); - Thread.sleep(100); - } - assertThat(n).isLessThan(100) - .withFailMessage("Message did not arrive in RabbitMQ"); - return bar; - } - - }; - } - - private RuntimeException bigCause(RuntimeException cause) { - if (getStackTraceAsString(cause).length() > this.maxStackTraceSize) { - return cause; - } - return bigCause(new RuntimeException(BIG_EXCEPTION_MESSAGE, cause)); - } - - private String getStackTraceAsString(Throwable cause) { - StringWriter stringWriter = new StringWriter(); - PrintWriter printWriter = new PrintWriter(stringWriter, true); - cause.printStackTrace(printWriter); - return stringWriter.getBuffer().toString(); - } - - private void verifyAutoDeclareContextClear(RabbitTestBinder binder) { - ApplicationContext ctx = - TestUtils.getPropertyValue(binder, "binder.provisioningProvider.autoDeclareContext", - ApplicationContext.class); - assertThat(ctx.getBeansOfType(Declarable.class)).isEmpty(); - } - - private List> getBindingsBySource(String vhost, String name) { - return RestUtils.getBindingsBySource(client, uri, vhost, name); - } - - private Map getExchange(String vhost, String name) { - return RestUtils.getExchange(client, uri, vhost, name); - } - - private Map getQueue(String vhost, String name) { - return RestUtils.getQueue(client, uri, vhost, name); - } - - private boolean requiredConsumerState(long state, Map queue) { - Object consumers = queue.get("consumers"); - return state == 0 - ? consumers == null || (Integer) consumers == 0 - : consumers != null && (Integer) consumers == state; - } - - public static class TestPartitionKeyExtractorClass - implements PartitionKeyExtractorStrategy { - - @Override - public Object extractKey(Message message) { - return 0; - } - - } - - public static class TestPartitionSelectorClass implements PartitionSelectorStrategy { - - @Override - public int selectPartition(Object key, int partitionCount) { - return 0; - } - - } - - public static class Pojo { - - private String field; - - public Pojo() { - super(); - } - - public Pojo(String field) { - this.field = field; - } - - public String getField() { - return this.field; - } - - public void setField(String field) { - this.field = field; - } - - } - // @checkstyle:on - - public static class TestBatchingStrategy implements BatchingStrategy { - - private final List messages = new ArrayList<>(); - private String exchange; - private String routingKey; - private int currentSize; - - @Override - public MessageBatch addToBatch(String exchange, String routingKey, org.springframework.amqp.core.Message message) { - this.exchange = exchange; - this.routingKey = routingKey; - this.messages.add(message); - currentSize += message.getBody().length + 2; - - MessageBatch batch = null; - if (this.messages.size() == 5) { - batch = this.doReleaseBatch(); - } - - return batch; - } - - @Override - public Date nextRelease() { - return null; - } - - @Override - public Collection releaseBatches() { - MessageBatch batch = this.doReleaseBatch(); - return batch == null ? Collections.emptyList() : Collections.singletonList(batch); - } - - private MessageBatch doReleaseBatch() { - if (this.messages.size() < 1) { - return null; - } - else { - ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[this.currentSize]); - for (org.springframework.amqp.core.Message message: messages) { - byteBuffer.put(message.getBody()).putChar('\n'); - } - MessageBatch messageBatch = new MessageBatch(this.exchange, this.routingKey, - new org.springframework.amqp.core.Message(byteBuffer.array(), new MessageProperties())); - this.messages.clear(); - return messageBatch; - } - } - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java deleted file mode 100644 index f9ad11295..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit; - -import java.util.HashSet; -import java.util.Set; - -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.cloud.stream.binder.AbstractPollableConsumerTestBinder; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.PollableSource; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.integration.config.EnableIntegration; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.util.StringUtils; - -/** - * Test support class for {@link RabbitMessageChannelBinder}. - * - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author David Turanski - * @author Mark Fisher - */ -// @checkstyle:off -public class RabbitTestBinder extends - AbstractPollableConsumerTestBinder, ExtendedProducerProperties> { - - // @checkstyle:on - private final RabbitAdmin rabbitAdmin; - - private final Set prefixes = new HashSet<>(); - - private final Set queues = new HashSet(); - - private final Set exchanges = new HashSet(); - - private final AnnotationConfigApplicationContext applicationContext; - - public RabbitTestBinder(ConnectionFactory connectionFactory, - RabbitProperties rabbitProperties) { - this(connectionFactory, new RabbitMessageChannelBinder(connectionFactory, - rabbitProperties, new RabbitExchangeQueueProvisioner(connectionFactory))); - } - - public RabbitTestBinder(ConnectionFactory connectionFactory, - RabbitMessageChannelBinder binder) { - this.applicationContext = new AnnotationConfigApplicationContext(Config.class); - binder.setApplicationContext(this.applicationContext); - this.setPollableConsumerBinder(binder); - this.rabbitAdmin = new RabbitAdmin(connectionFactory); - } - - public AnnotationConfigApplicationContext getApplicationContext() { - return this.applicationContext; - } - - @Override - public Binding bindConsumer(String name, String group, - MessageChannel moduleInputChannel, - ExtendedConsumerProperties properties) { - captureConsumerResources(name, group, properties); - return super.bindConsumer(name, group, moduleInputChannel, properties); - } - - @Override - public Binding> bindPollableConsumer(String name, - String group, PollableSource inboundBindTarget, - ExtendedConsumerProperties properties) { - captureConsumerResources(name, group, properties); - return super.bindPollableConsumer(name, group, inboundBindTarget, properties); - } - - private void captureConsumerResources(String name, String group, - ExtendedConsumerProperties properties) { - String[] names = null; - if (group != null) { - if (properties.getExtension().isQueueNameGroupOnly()) { - this.queues.add(properties.getExtension().getPrefix() + group); - } - else { - if (properties.isMultiplex()) { - names = StringUtils.commaDelimitedListToStringArray(name); - for (String nayme : names) { - this.queues.add(properties.getExtension().getPrefix() - + nayme.trim() + "." + group); - } - } - else { - this.queues.add( - properties.getExtension().getPrefix() + name + "." + group); - } - } - } - if (names != null) { - for (String nayme : names) { - this.exchanges.add(properties.getExtension().getPrefix() + nayme.trim()); - } - } - else { - this.exchanges.add(properties.getExtension().getPrefix() + name); - } - this.prefixes.add(properties.getExtension().getPrefix()); - deadLetters(properties.getExtension()); - } - - @Override - public Binding bindProducer(String name, - MessageChannel moduleOutputChannel, - ExtendedProducerProperties properties) { - this.queues.add(properties.getExtension().getPrefix() + name + ".default"); - this.exchanges.add(properties.getExtension().getPrefix() + name); - if (properties.getRequiredGroups() != null) { - for (String group : properties.getRequiredGroups()) { - if (properties.getExtension().isQueueNameGroupOnly()) { - this.queues.add(properties.getExtension().getPrefix() + group); - } - else { - this.queues.add( - properties.getExtension().getPrefix() + name + "." + group); - } - } - } - this.prefixes.add(properties.getExtension().getPrefix()); - deadLetters(properties.getExtension()); - return super.bindProducer(name, moduleOutputChannel, properties); - } - - private void deadLetters(RabbitCommonProperties properties) { - if (properties.getDeadLetterExchange() != null) { - this.exchanges.add(properties.getDeadLetterExchange()); - } - if (properties.getDeadLetterQueueName() != null) { - this.queues.add(properties.getDeadLetterQueueName()); - } - } - - @Override - public void cleanup() { - for (String queue : this.queues) { - this.rabbitAdmin.deleteQueue(queue); - this.rabbitAdmin.deleteQueue(queue + ".dlq"); - // delete any partitioned queues - for (int i = 0; i < 10; i++) { - this.rabbitAdmin.deleteQueue(queue + "-" + i); - this.rabbitAdmin.deleteQueue(queue + "-" + i + ".dlq"); - } - } - for (String exchange : this.exchanges) { - this.rabbitAdmin.deleteExchange(exchange); - } - for (String prefix : this.prefixes) { - this.rabbitAdmin.deleteExchange(prefix + "DLX"); - } - this.applicationContext.close(); - } - - @Configuration(proxyBeanMethods = false) - @EnableIntegration - static class Config { - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestContainer.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestContainer.java deleted file mode 100644 index c7de2b242..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestContainer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.rabbit; - -import java.time.Duration; - -import org.testcontainers.containers.RabbitMQContainer; - -/** - * Provides a static {@link RabbitMQContainer} that can be shared across test classes. - * - * @author Chris Bono - */ -public class RabbitTestContainer { - - private static final RabbitMQContainer RABBITMQ; - static { - String image = "rabbitmq:3.11-management"; - String cache = System.getenv().get("IMAGE_CACHE"); - if (cache != null) { - image = cache + image; - } - RABBITMQ = new RabbitMQContainer(image) - .withExposedPorts(5672, 15672, 5552) - .withEnv("RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS", "-rabbitmq_stream advertised_host localhost") - .withPluginsEnabled("rabbitmq_stream") - .withStartupTimeout(Duration.ofMinutes(2)); - RABBITMQ.start(); - } - - /** - * Should be called early by test that wants to ensure a shared {@link RabbitMQContainer} is up and running. - */ - public static RabbitMQContainer sharedInstance() { - return RABBITMQ; - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RepublishUnitTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RepublishUnitTests.java deleted file mode 100644 index 65f65e8ca..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RepublishUnitTests.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.binder.rabbit; - -import java.io.IOException; -import java.util.Collections; - -import com.rabbitmq.client.Channel; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.rabbit.connection.Connection; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.boot.autoconfigure.amqp.RabbitProperties; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.support.ErrorMessage; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -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 - * @since 3.0.6 - * - */ -public class RepublishUnitTests { - - @Test - public void testBadRepublishSetting() throws IOException { - ConnectionFactory cf = mock(ConnectionFactory.class); - Connection conn = mock(Connection.class); - given(cf.createConnection()).willReturn(conn); - Channel channel = mock(Channel.class); - given(channel.isOpen()).willReturn(true); - given(channel.exchangeDeclarePassive("DLX")).willThrow(new IOException()); - given(conn.createChannel(false)).willReturn(channel); - RabbitProperties props = new RabbitProperties(); - RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder(cf, props, null); - RabbitConsumerProperties extension = new RabbitConsumerProperties(); - ExtendedConsumerProperties bindingProps = - new ExtendedConsumerProperties<>(extension); - MessageHandler handler = binder.getErrorMessageHandler(mock(ConsumerDestination.class), "foo", bindingProps); - ErrorMessage message = new ErrorMessage(new RuntimeException("test"), - Collections.singletonMap(IntegrationMessageHeaderAccessor.SOURCE_DATA, - new Message("foo".getBytes(), new MessageProperties()))); - handler.handleMessage(message); - handler.handleMessage(message); - verify(channel, times(1)).exchangeDeclarePassive("DLX"); - verify(channel, never()).basicPublish(any(), any(), eq(false), any(), any()); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RestUtils.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RestUtils.java deleted file mode 100644 index c99f059a6..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RestUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder.rabbit; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.List; -import java.util.Map; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.util.UriUtils; - -/** - * @author Gary Russell - * @since 4.0 - * - */ -public final class RestUtils { - - private RestUtils() { - } - - public static List> getBindingsBySource(WebClient client, URI uri, String vhost, String name) { - String exchange = "".equals(name) ? "amq.default" : name; - URI getUri = uri - .resolve("/api/exchanges/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" - + UriUtils.encodePathSegment(exchange, StandardCharsets.UTF_8) + "/bindings/source"); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>>() { - }) - .block(Duration.ofSeconds(10)); - } - - public static Map getExchange(WebClient client, URI uri, String vhost, String name) { - String exchange = "".equals(name) ? "amq.default" : name; - URI getUri = uri - .resolve("/api/exchanges/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" - + UriUtils.encodePathSegment(exchange, StandardCharsets.UTF_8)); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(Duration.ofSeconds(10)); - } - - public static Map getQueue(WebClient client, URI uri, String vhost, String name) { - URI getUri = uri - .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" - + UriUtils.encodePathSegment(name, StandardCharsets.UTF_8)); - return client.get() - .uri(getUri) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(Duration.ofSeconds(10)); - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java deleted file mode 100644 index 41063edb0..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.integration; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.function.Function; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.Mockito; -import org.testcontainers.containers.RabbitMQContainer; - -import org.springframework.amqp.core.DeclarableCustomizer; -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.actuate.amqp.RabbitHealthIndicator; -import org.springframework.boot.actuate.health.CompositeHealthContributor; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.Cloud; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; -import org.springframework.cloud.stream.binder.rabbit.RabbitTestContainer; -import org.springframework.cloud.stream.binder.rabbit.RestUtils; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.AlternateExchange; -import org.springframework.cloud.stream.binder.test.junit.rabbit.RabbitTestSupport; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; -import org.springframework.integration.amqp.inbound.AmqpMessageSource; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.handler.AbstractMessageHandler; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.retry.backoff.ExponentialBackOffPolicy; -import org.springframework.retry.policy.SimpleRetryPolicy; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * @author Marius Bogoevici - * @author Gary Russell - * @author Artem Bilan - * @author Soby Chacko - * @author Chris Bono - * @author Byungjun You - */ -public class RabbitBinderModuleTests { - - private static final RabbitMQContainer RABBITMQ = RabbitTestContainer.sharedInstance(); - - private static final ConnectionFactory MOCK_CONNECTION_FACTORY = mock(ConnectionFactory.class, Mockito.RETURNS_MOCKS); - - private static final WebClient client; - - private static final URI uri; - - static { - client = WebClient.builder() - .filter(ExchangeFilterFunctions - .basicAuthentication(RABBITMQ.getAdminUsername(), RABBITMQ.getAdminPassword())) - .build(); - try { - uri = new URI(RABBITMQ.getHttpUrl() + "/api/"); - } - catch (URISyntaxException ex) { - throw new IllegalStateException(ex); - } - } - - @RegisterExtension - private final RabbitTestSupport rabbitTestSupport = new RabbitTestSupport(true, RABBITMQ.getAmqpPort(), RABBITMQ.getHttpPort()); - - private ConfigurableApplicationContext context; - - @AfterEach - public void tearDown() { - if (context != null) { - context.close(); - context = null; - } - RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.deleteExchange("process-in-0"); - admin.deleteExchange("process-out-0"); - } - - @Test - public void testParentConnectionFactoryInheritedByDefault() throws Exception { - context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE).run("--server.port=0", - "--spring.rabbitmq.port=" + RABBITMQ.getAmqpPort(), - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.rabbit.binder.connection-name-prefix=foo", - "--spring.cloud.stream.rabbit.bindings.process-in-0.consumer.single-active-consumer=true"); - BinderFactory binderFactory = context.getBean(BinderFactory.class); - Binder binder = binderFactory.getBinder(null, MessageChannel.class); - assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); - DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); - CachingConnectionFactory binderConnectionFactory = (CachingConnectionFactory) binderFieldAccessor - .getPropertyValue("connectionFactory"); - assertThat(binderConnectionFactory).isInstanceOf(CachingConnectionFactory.class); - ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); - assertThat(binderConnectionFactory).isSameAs(connectionFactory); - - CompositeHealthContributor bindersHealthIndicator = context - .getBean("bindersHealthContributor", CompositeHealthContributor.class); - - assertThat(bindersHealthIndicator).isNotNull(); - - RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("rabbit"); - assertThat(indicator).isNotNull(); - assertThat(indicator.health().getStatus()) - .isEqualTo(Status.UP); - - ConnectionFactory publisherConnectionFactory = binderConnectionFactory - .getPublisherConnectionFactory(); - assertThat(TestUtils.getPropertyValue(publisherConnectionFactory, - "connection.target")).isNull(); - DirectChannel checkPf = new DirectChannel(); - Binding binding = ((RabbitMessageChannelBinder) binder) - .bindProducer("checkPF", checkPf, - new ExtendedProducerProperties<>(new RabbitProducerProperties())); - checkPf.send(new GenericMessage<>("foo".getBytes())); - binding.unbind(); - assertThat(TestUtils.getPropertyValue(publisherConnectionFactory, - "connection.target")).isNotNull(); - - CachingConnectionFactory cf = this.context - .getBean(CachingConnectionFactory.class); - ConnectionNameStrategy cns = TestUtils.getPropertyValue(cf, - "connectionNameStrategy", ConnectionNameStrategy.class); - assertThat(cns.obtainNewConnectionName(cf)).isEqualTo("foo#2"); - new RabbitAdmin(rabbitTestSupport.getResource()).deleteExchange("checkPF"); - checkCustomizedArgs(); - binderConnectionFactory.resetConnection(); - binderConnectionFactory.createConnection(); - checkCustomizedArgs(); - } - - @SuppressWarnings("unchecked") - private void checkCustomizedArgs() throws MalformedURLException, URISyntaxException, InterruptedException { - List> bindings = RestUtils.getBindingsBySource(client, uri, "/", "process-in-0"); - int n = 0; - while (n++ < 100 && bindings == null || bindings.size() < 1) { - Thread.sleep(100); - bindings = RestUtils.getBindingsBySource(client, uri, "/", "process-in-0"); - } - assertThat(bindings).isNotNull(); - assertThat((Map) bindings.get(0).get("arguments")).contains(entry("added.by", "customizer")); - Map exchange = RestUtils.getExchange(client, uri, "/", "process-in-0"); - assertThat((Map) exchange.get("arguments")).contains(entry("added.by", "customizer")); - Map queue = RestUtils.getQueue(client, uri, "/", (String) bindings.get(0).get("destination")); - Map args = (Map) queue.get("arguments"); - assertThat(args).contains(entry("added.by", "customizer")); - assertThat(args).contains(entry("x-single-active-consumer", Boolean.TRUE)); - } - - @Test - @SuppressWarnings("unchecked") - public void testParentConnectionFactoryInheritedByDefaultAndRabbitSettingsPropagated() { - context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE).run("--server.port=0", - "--spring.rabbitmq.port=" + RABBITMQ.getAmqpPort(), - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.source.group=someGroup", - "--spring.cloud.stream.bindings.process-in-0.group=someGroup", - "--spring.cloud.stream.rabbit.bindings.process-in-0.consumer.transacted=true", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.transacted=true"); - BinderFactory binderFactory = context.getBean(BinderFactory.class); - Binder binder = binderFactory.getBinder(null, MessageChannel.class); - assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); - BindingService bindingService = context.getBean(BindingService.class); - DirectFieldAccessor channelBindingServiceAccessor = new DirectFieldAccessor( - bindingService); - // @checkstyle:off - Map>> consumerBindings = (Map>>) channelBindingServiceAccessor - .getPropertyValue("consumerBindings"); - // @checkstyle:on - Binding inputBinding = consumerBindings.get("process-in-0").get(0); - assertThat(TestUtils.getPropertyValue(inputBinding, "lifecycle.beanName")) - .isEqualTo("setByCustomizer:someGroup"); - SimpleMessageListenerContainer container = TestUtils.getPropertyValue( - inputBinding, "lifecycle.messageListenerContainer", - SimpleMessageListenerContainer.class); - assertThat(TestUtils.getPropertyValue(container, "beanName")) - .isEqualTo("setByCustomizerForQueue:process-in-0.someGroup,andGroup:someGroup"); - assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)) - .isTrue(); - Map> producerBindings = (Map>) TestUtils - .getPropertyValue(bindingService, "producerBindings"); - Binding outputBinding = producerBindings.get("process-out-0"); - assertThat(TestUtils.getPropertyValue(outputBinding, - "lifecycle.amqpTemplate.transactional", Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(outputBinding, "lifecycle.beanName")) - .isEqualTo("setByCustomizer:process-out-0"); - DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); - ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor - .getPropertyValue("connectionFactory"); - assertThat(binderConnectionFactory).isInstanceOf(CachingConnectionFactory.class); - ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); - assertThat(binderConnectionFactory).isSameAs(connectionFactory); - CompositeHealthContributor bindersHealthIndicator = context - .getBean("bindersHealthContributor", CompositeHealthContributor.class); - - assertThat(bindersHealthIndicator).isNotNull(); - - RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("rabbit"); - assertThat(indicator).isNotNull(); - assertThat(indicator.health().getStatus()) - .isEqualTo(Status.UP); - - CachingConnectionFactory cf = this.context - .getBean(CachingConnectionFactory.class); - ConnectionNameStrategy cns = TestUtils.getPropertyValue(cf, - "connectionNameStrategy", ConnectionNameStrategy.class); - assertThat(cns.obtainNewConnectionName(cf)).startsWith("rabbitConnectionFactory"); -// assertThat(TestUtils.getPropertyValue(consumerBindings.get("source").get(0), -// "target.source.h.advised.targetSource.target.beanName")) -// .isEqualTo("setByCustomizer:someGroup"); - } - - @Test - public void testParentConnectionFactoryInheritedIfOverridden() { - context = new SpringApplicationBuilder(SimpleProcessor.class, - ConnectionFactoryConfiguration.class).web(WebApplicationType.NONE) - .run("--server.port=0", "--spring.rabbitmq.port=" + RABBITMQ.getAmqpPort()); - BinderFactory binderFactory = context.getBean(BinderFactory.class); - Binder binder = binderFactory.getBinder(null, MessageChannel.class); - assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); - DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); - ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor - .getPropertyValue("connectionFactory"); - assertThat(binderConnectionFactory).isSameAs(MOCK_CONNECTION_FACTORY); - ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); - assertThat(binderConnectionFactory).isSameAs(connectionFactory); - CompositeHealthContributor bindersHealthIndicator = context - .getBean("bindersHealthContributor", CompositeHealthContributor.class); - assertThat(bindersHealthIndicator).isNotNull(); - RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("rabbit"); - assertThat(indicator).isNotNull(); - // mock connection factory behaves as if down - assertThat(indicator.health().getStatus()) - .isEqualTo(Status.DOWN); - } - - @Test - public void testParentConnectionFactoryNotInheritedByCustomizedBindersAndProducerRetryBootProperties() { - List params = new ArrayList<>(); - params.add("--spring.cloud.function.definition=process"); - params.add("--spring.cloud.stream.process-in-0.binder=custom"); - params.add("--spring.cloud.stream.process-out-0.binder=custom"); - params.add("--spring.cloud.stream.binders.custom.type=rabbit"); - params.add("--spring.cloud.stream.binders.custom.environment.foo=bar"); - params.add("--server.port=0"); - params.add("--spring.rabbitmq.port=" + RABBITMQ.getAmqpPort()); - params.add("--spring.rabbitmq.template.retry.enabled=true"); - params.add("--spring.rabbitmq.template.retry.maxAttempts=2"); - params.add("--spring.rabbitmq.template.retry.initial-interval=1000"); - params.add("--spring.rabbitmq.template.retry.multiplier=1.1"); - params.add("--spring.rabbitmq.template.retry.max-interval=3000"); - context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE) - .run(params.toArray(new String[0])); - BinderFactory binderFactory = context.getBean(BinderFactory.class); - // @checkstyle:off - @SuppressWarnings("unchecked") - Binder, ExtendedProducerProperties> binder = (Binder, ExtendedProducerProperties>) binderFactory - .getBinder(null, MessageChannel.class); - // @checkstyle:on - assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); - DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); - ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor - .getPropertyValue("connectionFactory"); - ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); - assertThat(binderConnectionFactory).isNotSameAs(connectionFactory); - CompositeHealthContributor bindersHealthIndicator = context - .getBean("bindersHealthContributor", CompositeHealthContributor.class); - assertThat(bindersHealthIndicator).isNotNull(); - - RabbitHealthIndicator indicator = (RabbitHealthIndicator) bindersHealthIndicator.getContributor("custom"); - assertThat(indicator).isNotNull(); - assertThat(indicator.health().getStatus()).isEqualTo(Status.UP); - String name = UUID.randomUUID().toString(); - Binding binding = binder.bindProducer(name, new DirectChannel(), - new ExtendedProducerProperties<>(new RabbitProducerProperties())); - RetryTemplate template = TestUtils.getPropertyValue(binding, - "lifecycle.amqpTemplate.retryTemplate", RetryTemplate.class); - assertThat(template).isNotNull(); - SimpleRetryPolicy retryPolicy = TestUtils.getPropertyValue(template, - "retryPolicy", SimpleRetryPolicy.class); - ExponentialBackOffPolicy backOff = TestUtils.getPropertyValue(template, - "backOffPolicy", ExponentialBackOffPolicy.class); - assertThat(retryPolicy.getMaxAttempts()).isEqualTo(2); - assertThat(backOff.getInitialInterval()).isEqualTo(1000L); - assertThat(backOff.getMultiplier()).isEqualTo(1.1); - assertThat(backOff.getMaxInterval()).isEqualTo(3000L); - binding.unbind(); - new RabbitAdmin(rabbitTestSupport.getResource()).deleteExchange(name); - context.close(); - } - - @Test - public void testCloudProfile() { - this.context = new SpringApplicationBuilder(SimpleProcessor.class, - MockCloudConfiguration.class).web(WebApplicationType.NONE) - .profiles("cloud").run(); - BinderFactory binderFactory = this.context.getBean(BinderFactory.class); - Binder binder = binderFactory.getBinder(null, MessageChannel.class); - assertThat(binder).isInstanceOf(RabbitMessageChannelBinder.class); - DirectFieldAccessor binderFieldAccessor = new DirectFieldAccessor(binder); - ConnectionFactory binderConnectionFactory = (ConnectionFactory) binderFieldAccessor - .getPropertyValue("connectionFactory"); - ConnectionFactory connectionFactory = this.context - .getBean(ConnectionFactory.class); - - assertThat(binderConnectionFactory).isNotSameAs(connectionFactory); - - assertThat(TestUtils.getPropertyValue(connectionFactory, "addresses")) - .isNotNull(); - assertThat(TestUtils.getPropertyValue(binderConnectionFactory, "addresses")) - .isNull(); - - Cloud cloud = this.context.getBean(Cloud.class); - - verify(cloud).getSingletonServiceConnector(ConnectionFactory.class, null); - } - - @Test - public void testExtendedProperties() { - context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE).run("--server.port=0", - "--spring.rabbitmq.port=" + RABBITMQ.getAmqpPort(), - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.rabbit.default.producer.routing-key-expression=fooRoutingKey", - "--spring.cloud.stream.rabbit.default.consumer.exchange-type=direct", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.batch-size=512", - "--spring.cloud.stream.rabbit.default.consumer.max-concurrency=4", - "--spring.cloud.stream.rabbit.bindings.process-in-0.consumer.exchange-type=fanout", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.alternateExchange.name=altEx", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.alternate-exchange.exists=true", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.alternate-exchange.type=direct", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.alternate-exchange.binding.queue=altQ", - "--spring.cloud.stream.rabbit.bindings.process-out-0.producer.alternate-exchange.binding.routing-key=altRK"); - BinderFactory binderFactory = context.getBean(BinderFactory.class); - Binder rabbitBinder = binderFactory.getBinder(null, - MessageChannel.class); - - RabbitProducerProperties rabbitProducerProperties = (RabbitProducerProperties) ((ExtendedPropertiesBinder) rabbitBinder) - .getExtendedProducerProperties("process-out-0"); - - assertThat( - rabbitProducerProperties.getRoutingKeyExpression().getExpressionString()) - .isEqualTo("fooRoutingKey"); - assertThat(rabbitProducerProperties.getBatchSize()).isEqualTo(512); - AlternateExchange alternate = rabbitProducerProperties.getAlternateExchange(); - assertThat(alternate.getName()).isEqualTo("altEx"); - assertThat(alternate.isExists()).isTrue(); - assertThat(alternate.getType()).isEqualTo("direct"); - assertThat(alternate.getBinding().getQueue()).isEqualTo("altQ"); - assertThat(alternate.getBinding().getRoutingKey()).isEqualTo("altRK"); - - RabbitConsumerProperties rabbitConsumerProperties = (RabbitConsumerProperties) ((ExtendedPropertiesBinder) rabbitBinder) - .getExtendedConsumerProperties("process-in-0"); - - assertThat(rabbitConsumerProperties.getExchangeType()) - .isEqualTo(ExchangeTypes.FANOUT); - assertThat(rabbitConsumerProperties.getMaxConcurrency()).isEqualTo(4); - } - - @SpringBootApplication - public static class SimpleProcessor { - - @Bean - public ListenerContainerCustomizer containerCustomizer() { - return (c, q, g) -> { - ((AbstractMessageListenerContainer) c).setBeanName( - "setByCustomizerForQueue:" + q + (g == null ? "" : ",andGroup:" + g)); - }; - } - - @Bean - public MessageSourceCustomizer sourceCustomizer() { - return (s, q, g) -> s.setBeanName("setByCustomizer:" + g); - } - - @Bean - public ProducerMessageHandlerCustomizer messageHandlerCustomizer() { - return (handler, destinationName) -> { - if (handler instanceof AbstractMessageHandler messageHandler) { - messageHandler.setBeanName("setByCustomizer:" + destinationName); - } - }; - } - - @Bean - public ConsumerEndpointCustomizer adapterCustomizer() { - return (producer, dest, grp) -> producer.setBeanName("setByCustomizer:" + grp); - } - - @Bean - public DeclarableCustomizer customizer() { - return dec -> { - dec.addArgument("added.by", "customizer"); - return dec; - }; - } - - @Bean - public Function process() { - return s -> s; - } - } - - public static class ConnectionFactoryConfiguration { - - @Bean - public ConnectionFactory connectionFactory() { - return MOCK_CONNECTION_FACTORY; - } - - } - - public static class MockCloudConfiguration { - - @Bean - public Cloud cloud() { - Cloud cloud = mock(Cloud.class); - - willReturn(new CachingConnectionFactory("localhost")).given(cloud) - .getSingletonServiceConnector(ConnectionFactory.class, null); - - return cloud; - } - - } -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamBinderModuleTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamBinderModuleTests.java deleted file mode 100644 index d4d2d9819..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream/RabbitStreamBinderModuleTests.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.stream; - -import com.rabbitmq.stream.ConsumerBuilder; -import com.rabbitmq.stream.Environment; -import com.rabbitmq.stream.OffsetSpecification; -import com.rabbitmq.stream.ProducerBuilder; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.RabbitMQContainer; - -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; -import org.springframework.cloud.stream.binder.rabbit.RabbitTestContainer; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties.ProducerType; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.amqp.outbound.RabbitStreamMessageHandler; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.rabbit.stream.listener.StreamListenerContainer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * @author Gary Russell - */ -public class RabbitStreamBinderModuleTests { - - private static final RabbitMQContainer RABBITMQ = RabbitTestContainer.sharedInstance(); - - @Test - void testStreamContainer() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE) - .run("--server.port=0")) { - BinderFactory binderFactory = context.getBean(BinderFactory.class); - RabbitMessageChannelBinder rabbitBinder = (RabbitMessageChannelBinder) binderFactory.getBinder(null, - MessageChannel.class); - RabbitConsumerProperties rProps = new RabbitConsumerProperties(); - rProps.setContainerType(ContainerType.STREAM); - ExtendedConsumerProperties props = - new ExtendedConsumerProperties(rProps); - props.setAutoStartup(false); - Binding binding = rabbitBinder.bindConsumer("testStream", "grp", new QueueChannel(), props); - Object container = TestUtils.getPropertyValue(binding, "lifecycle.messageListenerContainer"); - assertThat(container).isInstanceOf(StreamListenerContainer.class); - ((StreamListenerContainer) container).start(); - verify(context.getBean(ConsumerBuilder.class)).offset(OffsetSpecification.first()); - ((StreamListenerContainer) container).stop(); - } - } - - @Test - void testSuperStreamContainer() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE) - .run("--server.port=0")) { - BinderFactory binderFactory = context.getBean(BinderFactory.class); - RabbitMessageChannelBinder rabbitBinder = (RabbitMessageChannelBinder) binderFactory.getBinder(null, - MessageChannel.class); - RabbitConsumerProperties rProps = new RabbitConsumerProperties(); - rProps.setContainerType(ContainerType.STREAM); - rProps.setSuperStream(true); - ExtendedConsumerProperties props = - new ExtendedConsumerProperties(rProps); - props.setAutoStartup(false); - props.setInstanceCount(1); - Binding binding = rabbitBinder.bindConsumer("testSuperStream", "grp", new QueueChannel(), props); - Object container = TestUtils.getPropertyValue(binding, "lifecycle.messageListenerContainer"); - assertThat(container).isInstanceOf(StreamListenerContainer.class); - ((StreamListenerContainer) container).start(); - ConsumerBuilder builder = context.getBean(ConsumerBuilder.class); - verify(builder).singleActiveConsumer(); - verify(builder).superStream("testSuperStream"); - verify(builder).name("testSuperStream.grp"); - ((StreamListenerContainer) container).stop(); - } - } - - @Test - void testStreamHandler() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE) - .run("--server.port=0")) { - BinderFactory binderFactory = context.getBean(BinderFactory.class); - RabbitMessageChannelBinder rabbitBinder = (RabbitMessageChannelBinder) binderFactory.getBinder(null, - MessageChannel.class); - RabbitProducerProperties rProps = new RabbitProducerProperties(); - rProps.setProducerType(ProducerType.STREAM_SYNC); - ExtendedProducerProperties props = - new ExtendedProducerProperties(rProps); - Binding binding = rabbitBinder.bindProducer("testStream", new DirectChannel(), props); - Object handler = TestUtils.getPropertyValue(binding, "val$producerMessageHandler"); - assertThat(handler).isInstanceOf(RabbitStreamMessageHandler.class); - } - } - - @SpringBootApplication(proxyBeanMethods = false) - public static class SimpleProcessor { - - @Bean - ConnectionFactory cf() { - return new CachingConnectionFactory(RABBITMQ.getMappedPort(5672)); - } - - @Bean - ProducerMessageHandlerCustomizer handlerCustomizer() { - return (hand, dest) -> { - RabbitStreamMessageHandler handler = (RabbitStreamMessageHandler) hand; - handler.setConfirmTimeout(5000); - }; - } - - @Bean - ListenerContainerCustomizer containerCustomizer() { - return (cont, dest, group) -> { - StreamListenerContainer container = (StreamListenerContainer) cont; - container.setConsumerCustomizer((name, builder) -> { - builder.offset(OffsetSpecification.first()); - }); - }; - } - - @Bean - Environment rabbitStreamEnvironment(ConsumerBuilder consumerBuilder, ProducerBuilder producerBuilder) { - Environment env = mock(Environment.class); - given(env.consumerBuilder()).willReturn(consumerBuilder); - given(env.producerBuilder()).willReturn(producerBuilder); - return env; - } - - @Bean - ConsumerBuilder consumerBuilder() { - ConsumerBuilder mock = mock(ConsumerBuilder.class); - given(mock.superStream(anyString())).willReturn(mock); - given(mock.singleActiveConsumer()).willReturn(mock); - return mock; - } - - @Bean - ProducerBuilder producerBuilder() { - return mock(ProducerBuilder.class); - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream2/RabbitStreamBinderModuleIntegrationTests.java b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream2/RabbitStreamBinderModuleIntegrationTests.java deleted file mode 100644 index a56e126de..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/stream2/RabbitStreamBinderModuleIntegrationTests.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.rabbit.stream2; - -import java.util.Properties; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import com.rabbitmq.stream.Address; -import com.rabbitmq.stream.OffsetSpecification; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.RabbitMQContainer; - -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.listener.MessageListenerContainer; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.amqp.EnvironmentBuilderCustomizer; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.RabbitMessageChannelBinder; -import org.springframework.cloud.stream.binder.rabbit.RabbitTestContainer; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties; -import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties.ContainerType; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.messaging.MessageChannel; -import org.springframework.rabbit.stream.listener.StreamListenerContainer; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Gary Russell - */ -public class RabbitStreamBinderModuleIntegrationTests { - - private static final RabbitMQContainer RABBITMQ = RabbitTestContainer.sharedInstance(); - - @Test - void testSuperStreamContainer() throws InterruptedException { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(SimpleProcessor.class) - .web(WebApplicationType.NONE) - .run("--server.port=0")) { - BinderFactory binderFactory = context.getBean(BinderFactory.class); - RabbitMessageChannelBinder rabbitBinder = (RabbitMessageChannelBinder) binderFactory.getBinder(null, - MessageChannel.class); - RabbitConsumerProperties rProps = new RabbitConsumerProperties(); - rProps.setContainerType(ContainerType.STREAM); - rProps.setSuperStream(true); - ExtendedConsumerProperties props = - new ExtendedConsumerProperties(rProps); - props.setAutoStartup(false); - props.setInstanceCount(3); - props.setConcurrency(3); - Binding binding = rabbitBinder.bindConsumer("testSuperStream", "grp", new QueueChannel(), props); - Object container = TestUtils.getPropertyValue(binding, "lifecycle.messageListenerContainer"); - assertThat(container).isInstanceOf(StreamListenerContainer.class); - assertThat(container).extracting("concurrency").isEqualTo(3); - ((StreamListenerContainer) container).start(); - assertThat(context.getBean(SimpleProcessor.class).consumerCountLatch.await(10, TimeUnit.SECONDS)).isTrue(); - RabbitAdmin admin = context.getBean(RabbitAdmin.class); - System.out.println(RABBITMQ.getMappedPort(15672)); - for (int i = 0; i < 9; i++) { - Properties qProps = admin.getQueueProperties("testSuperStream-" + i); - assertThat(qProps).describedAs("Expected queue with index %d to exist", i).isNotNull(); - } - ((StreamListenerContainer) container).stop(); - } - } - - @SpringBootApplication(proxyBeanMethods = false) - public static class SimpleProcessor { - - final CountDownLatch consumerCountLatch = new CountDownLatch(3); - - @Bean - ConnectionFactory cf() { - return new CachingConnectionFactory(RABBITMQ.getMappedPort(5672)); - } - - @Bean - RabbitAdmin admin(ConnectionFactory cf) { - return new RabbitAdmin(cf); - } - - @Bean - EnvironmentBuilderCustomizer environmenCustomizer() { - return env -> env - .addressResolver(add -> new Address("localhost", RABBITMQ.getMappedPort(5552))); - } - - @Bean - ListenerContainerCustomizer containerCustomizer() { - return (cont, dest, group) -> { - if (cont instanceof StreamListenerContainer container) { - container.setConsumerCustomizer((id, builder) -> { - builder.consumerUpdateListener(context -> { - this.consumerCountLatch.countDown(); - return OffsetSpecification.first(); - }); - }); - } - }; - } - - } - -} diff --git a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/resources/log4j.properties b/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/resources/log4j.properties deleted file mode 100644 index 335956bc0..000000000 --- a/binders/rabbit-binder/spring-cloud-stream-binder-rabbit/src/test/resources/log4j.properties +++ /dev/null @@ -1,8 +0,0 @@ -log4j.rootCategory=DEBUG, stdout - -# standard logging including calling site -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %40.40c:%4L - %m%n - -log4j.category.org.springframework.cloud.binder.rabbit=DEBUG diff --git a/binders/rabbit-binder/src/checkstyle/checkstyle-suppressions.xml b/binders/rabbit-binder/src/checkstyle/checkstyle-suppressions.xml deleted file mode 100644 index 9d0be9549..000000000 --- a/binders/rabbit-binder/src/checkstyle/checkstyle-suppressions.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bom/.editorconfig b/bom/.editorconfig deleted file mode 100644 index e65ee4533..000000000 --- a/bom/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = tab -indent_size = 4 -end_of_line = lf -insert_final_newline = true - -[*.yml] -indent_style = space -indent_size = 2 diff --git a/bom/.gitignore b/bom/.gitignore deleted file mode 100644 index 62c74700a..000000000 --- a/bom/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -*~ -#* -*# -.#* -.classpath -.project -.settings -.springBeans -.gradle -build -bin -target/ -.idea -*.iml -*.ipr -*.iws -.factorypath -*.versionsBackup -*.DS_Store diff --git a/bom/.gitmodules b/bom/.gitmodules deleted file mode 100644 index b87e34601..000000000 --- a/bom/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "spring-cloud-stream-docs/kafka"] - path = spring-cloud-stream-docs/kafka - url = https://github.com/spring-cloud/spring-cloud-stream-binder-kafka.git -[submodule "spring-cloud-stream-docs/rabbit"] - path = spring-cloud-stream-docs/rabbit - url = https://github.com/spring-cloud/spring-cloud-stream-binder-rabbit.git -[submodule "spring-cloud-stream-docs/core"] - path = spring-cloud-stream-docs/core - url = https://github.com/spring-cloud/spring-cloud-stream.git diff --git a/bom/.mvn/jvm.config b/bom/.mvn/jvm.config deleted file mode 100644 index 0e7dabeff..000000000 --- a/bom/.mvn/jvm.config +++ /dev/null @@ -1 +0,0 @@ --Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom \ No newline at end of file diff --git a/bom/.mvn/maven.config b/bom/.mvn/maven.config deleted file mode 100644 index 3b8cf46e1..000000000 --- a/bom/.mvn/maven.config +++ /dev/null @@ -1 +0,0 @@ --DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring diff --git a/bom/.mvn/wrapper/MavenWrapperDownloader.java b/bom/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100755 index 2e394d5b3..000000000 --- a/bom/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,110 +0,0 @@ -/* -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you under the Apache License, Version 2.0 (the -"License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. -*/ - -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = - "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: : " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/bom/.mvn/wrapper/maven-wrapper.jar b/bom/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index 01e679973..000000000 Binary files a/bom/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/bom/.mvn/wrapper/maven-wrapper.properties b/bom/.mvn/wrapper/maven-wrapper.properties deleted file mode 100755 index 00d32aab1..000000000 --- a/bom/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file diff --git a/bom/.springformat b/bom/.springformat deleted file mode 100644 index e69de29bb..000000000 diff --git a/bom/LICENSE.txt b/bom/LICENSE.txt deleted file mode 100644 index 62589edd1..000000000 --- a/bom/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/bom/README.adoc b/bom/README.adoc deleted file mode 100644 index 6d59ba2fd..000000000 --- a/bom/README.adoc +++ /dev/null @@ -1,30 +0,0 @@ -== What is this? - -This is the repository for the Spring Cloud Stream Release Train. - -It contains projects for maintaining the following components: - -* the curated dependencies list of the release train, i.e. `spring-cloud-stream-dependencies` -* the aggregated documentation of the release train; - -== Release Train Contents - -The release train contains: - -* the Spring Cloud Stream core; -* supported binder implementations; - -=== Binder implementations - -The following binder implementations are currently available: - -* *Kafka* https://github.com/spring-cloud/spring-cloud-stream-binder-kafka -* *RabbitMQ* https://github.com/spring-cloud/spring-cloud-stream-binder-rabbit - -== Contributing - -We love contributions. -Follow this https://github.com/spring-cloud/spring-cloud-commons#contributing[link] for more information on how to contribute. - -== Code of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-stream/blob/master/CODE_OF_CONDUCT.adoc[code of conduct]. By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. diff --git a/bom/mvnw b/bom/mvnw deleted file mode 100755 index 8b9da3b8b..000000000 --- a/bom/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - 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" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/bom/mvnw.cmd b/bom/mvnw.cmd deleted file mode 100755 index a5284c793..000000000 --- a/bom/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/bom/pom.xml b/bom/pom.xml deleted file mode 100644 index 635791910..000000000 --- a/bom/pom.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - 4.0.0 - spring-cloud-stream-starter-build - pom - spring-cloud-stream-release-build - Spring Cloud Stream Release Build - 4.1.0-SNAPSHOT - - org.springframework.cloud - spring-cloud-build - 4.1.0-SNAPSHOT - - - - - https://github.com/spring-cloud/spring-cloud-stream-starters - - scm:git:git://github.com/spring-cloud/spring-cloud-stream-starters.git - - - scm:git:ssh://git@github.com/spring-cloud/spring-cloud-stream-starters.git - - HEAD - - - - true - true - - true - - - - - spring-cloud-stream-dependencies - spring-cloud-starter-parent - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-rules - - enforce - - - - - - commons-logging:*:* - - true - - - - true - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - io.spring.javaformat - spring-javaformat-maven-plugin - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - - spring - - true - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - - - - diff --git a/bom/spring-cloud-starter-parent/pom.xml b/bom/spring-cloud-starter-parent/pom.xml deleted file mode 100644 index 4f7dce46d..000000000 --- a/bom/spring-cloud-starter-parent/pom.xml +++ /dev/null @@ -1,151 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.2.0-SNAPSHOT - - - org.springframework.cloud - spring-cloud-stream-starter-parent - 4.1.0-SNAPSHOT - spring-cloud-stream-starter-parent - Specifies Boot version for the releaser - pom - https://projects.spring.io/spring-cloud - - Pivotal Software, Inc. - https://www.spring.io - - - ${basedir}/../.. - - - https://github.com/spring-cloud - - spring-docs - - scp://static.springframework.org/var/www/domains/springframework.org/static/htdocs/spring-cloud/docs/${project.artifactId}/${project.version} - - - - repo.spring.io - Spring Release Repository - https://repo.spring.io/libs-release-local - - - repo.spring.io - Spring Snapshot Repository - https://repo.spring.io/libs-snapshot-local - - - - - spring - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - - - milestone - - - repo.spring.io - Spring Milestone Repository - https://repo.spring.io/libs-milestone-local - - - - - bintray - - - bintray - Jcenter Repository - - https://api.bintray.com/maven/spring/jars/org.springframework.cloud:${bintray.package} - - - - - - central - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - - - - diff --git a/bom/spring-cloud-stream-dependencies/pom.xml b/bom/spring-cloud-stream-dependencies/pom.xml deleted file mode 100644 index f19ec8fff..000000000 --- a/bom/spring-cloud-stream-dependencies/pom.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - 4.0.0 - - spring-cloud-dependencies-parent - org.springframework.cloud - 4.1.0-SNAPSHOT - - - spring-cloud-stream-dependencies - 4.1.0-SNAPSHOT - pom - spring-cloud-stream-dependencies - Spring Cloud Stream Dependencies - - - - - - org.springframework.cloud - spring-cloud-stream - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-test-binder - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-test-support - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-kafka - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-reactive - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-streams - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-kafka-core - ${project.version} - - - org.springframework.cloud - spring-cloud-starter-stream-kafka - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-core - ${project.version} - - - org.springframework.cloud - spring-cloud-starter-stream-rabbit - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-binder-rabbit-test-support - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-schema-registry-client - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-schema-registry-core - ${project.version} - - - org.springframework.cloud - spring-cloud-stream-schema-registry-server - ${project.version} - - - - - - spring - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - - - diff --git a/buildCore.sh b/buildCore.sh deleted file mode 100755 index f284cecdc..000000000 --- a/buildCore.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [ "$#" -eq 1 ]; then - ARGVALUE=$1 - if [[ $ARGVALUE == *"skipTest"* ]]; then - echo "Skippping Tests" - ./mvnw clean install -f core/pom.xml -DskipTests - elif [[ $ARGVALUE == *"disable.checks"* ]]; then - echo "Skippping checkstyle checks" - ./mvnw clean install -f core/pom.xml -Ddisable.checks=true - fi -else - ./mvnw clean install -f core/pom.xml -fi diff --git a/buildDependencies.sh b/buildDependencies.sh deleted file mode 100755 index 2611686ab..000000000 --- a/buildDependencies.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -./mvnw clean install -f bom/pom.xml diff --git a/buildKafkaBinder.sh b/buildKafkaBinder.sh deleted file mode 100755 index 08050e93b..000000000 --- a/buildKafkaBinder.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [ "$#" -eq 1 ]; then - ARGVALUE=$1 - if [[ $ARGVALUE == *"skipTest"* ]]; then - echo "Skippping Tests" - ./mvnw clean install -f binders/kafka-binder/pom.xml -DskipTests - elif [[ $ARGVALUE == *"disable.checks"* ]]; then - echo "Skippping checkstyle checks" - ./mvnw clean install -f binders/kafka-binder/pom.xml -Ddisable.checks=true - fi -else - ./mvnw clean install -f binders/kafka-binder/pom.xml -fi diff --git a/buildRabbitBinder.sh b/buildRabbitBinder.sh deleted file mode 100755 index d8d56e568..000000000 --- a/buildRabbitBinder.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [ "$#" -eq 1 ]; then - ARGVALUE=$1 - if [[ $ARGVALUE == *"skipTest"* ]]; then - echo "Skippping Tests" - ./mvnw clean install -f core/pom.xml -DskipTests - elif [[ $ARGVALUE == *"disable.checks"* ]]; then - echo "Skippping checkstyle checks" - ./mvnw clean install -f core/pom.xml -Ddisable.checks=true - fi -else - ./mvnw clean install -f binders/rabbit-binder/pom.xml -fi diff --git a/buildSchemaRegistry.sh b/buildSchemaRegistry.sh deleted file mode 100755 index 7ca1f8e18..000000000 --- a/buildSchemaRegistry.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [ "$#" -eq 1 ]; then - ARGVALUE=$1 - if [[ $ARGVALUE == *"skipTest"* ]]; then - echo "Skippping Tests" - ./mvnw clean install -f schema-registry/pom.xml -DskipTests - elif [[ $ARGVALUE == *"disable.checks"* ]]; then - echo "Skippping checkstyle checks" - ./mvnw clean install -f schema-registry/pom.xml -Ddisable.checks=true - fi -else - ./mvnw clean install -f schema-registry/pom.xml -fi diff --git a/core/pom.xml b/core/pom.xml deleted file mode 100644 index b3eb11607..000000000 --- a/core/pom.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - 4.0.0 - spring-cloud-stream-core - 4.1.0-SNAPSHOT - core - Core Spring Cloud Stream - pom - - - org.springframework.cloud - spring-cloud-stream-parent - 4.1.0-SNAPSHOT - .. - - - - spring-cloud-stream - spring-cloud-stream-test-binder - spring-cloud-stream-integration-tests - spring-cloud-stream-test-support - - - diff --git a/core/spring-cloud-stream-integration-tests/pom.xml b/core/spring-cloud-stream-integration-tests/pom.xml deleted file mode 100644 index 0ffce3390..000000000 --- a/core/spring-cloud-stream-integration-tests/pom.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-integration-tests - jar - spring-cloud-stream-integration-tests - Spring Cloud Stream Integration Tests using Test Binder - - - org.springframework.cloud - spring-cloud-stream-core - 4.1.0-SNAPSHOT - - - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.cloud - spring-cloud-stream-test-binder - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.integration - spring-integration-test - test - - - org.springframework.kafka - spring-kafka - test - - - org.jetbrains.kotlin - kotlin-test - - test - - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - - - compile - compile - - compile - - - - src/main/java - src/test/kotlin - - - - - test-compile - test-compile - - test-compile - - - - - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - - - diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/BasicAbstractMessageChannelBinderTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/BasicAbstractMessageChannelBinderTests.java deleted file mode 100644 index 658e32e85..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/BasicAbstractMessageChannelBinderTests.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder; - -import java.util.Iterator; -import java.util.Set; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.provisioning.ProvisioningProvider; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.Lifecycle; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.handler.BridgeHandler; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.SubscribableChannel; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Marius Bogoevici - * @author Gary Russell - * @author Oleg Zhurakousky - * @since 1.2.2 - */ -@Disabled("This test has issues that needs to be looked into") -public class BasicAbstractMessageChannelBinderTests { - - private static ApplicationContext context; - - @BeforeAll - public static void prepare() { - context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration()) - .web(WebApplicationType.NONE).run(); - } - - @Test - @SuppressWarnings("unchecked") - public void testEndpointLifecycle() throws Exception { - // @checkstyle:off - AbstractMessageChannelBinder> binder = context - .getBean(AbstractMessageChannelBinder.class); - // @checkstyle:on - - ConsumerProperties consumerProperties = new ConsumerProperties(); - consumerProperties.setMaxAttempts(1); // to force error infrastructure creation - - Binding consumerBinding = binder.bindConsumer("foo", "fooGroup", - new DirectChannel(), consumerProperties); - DirectFieldAccessor consumerBindingAccessor = new DirectFieldAccessor( - consumerBinding); - MessageProducer messageProducer = (MessageProducer) consumerBindingAccessor - .getPropertyValue("lifecycle"); - assertThat(((Lifecycle) messageProducer).isRunning()).isTrue(); - assertThat(messageProducer.getOutputChannel()).isNotNull(); - - SubscribableChannel errorChannel = (SubscribableChannel) consumerBindingAccessor - .getPropertyValue("lifecycle.errorChannel"); - assertThat(errorChannel).isNotNull(); - Set handlers = TestUtils.getPropertyValue(errorChannel, - "dispatcher.handlers", Set.class); - assertThat(handlers.size()).isEqualTo(2); - Iterator iterator = handlers.iterator(); - assertThat(iterator.next()).isInstanceOf(BridgeHandler.class); - assertThat(iterator.next()).isInstanceOf(LastSubscriberMessageHandler.class); - assertThat(this.context.containsBean("foo.fooGroup.errors")).isTrue(); - assertThat(this.context.containsBean("foo.fooGroup.errors.recoverer")).isTrue(); - assertThat(this.context.containsBean("foo.fooGroup.errors.handler")).isTrue(); - assertThat(this.context.containsBean("foo.fooGroup.errors.bridge")).isTrue(); - consumerBinding.unbind(); - assertThat(this.context.containsBean("foo.fooGroup.errors")).isFalse(); - assertThat(this.context.containsBean("foo.fooGroup.errors.recoverer")).isFalse(); - assertThat(this.context.containsBean("foo.fooGroup.errors.handler")).isFalse(); - assertThat(this.context.containsBean("foo.fooGroup.errors.bridge")).isFalse(); - - assertThat(((Lifecycle) messageProducer).isRunning()).isFalse(); - - ProducerProperties producerProps = new ProducerProperties(); - producerProps.setErrorChannelEnabled(true); - Binding producerBinding = binder.bindProducer("bar", - new DirectChannel(), producerProps); - assertThat(this.context.containsBean("bar.errors")).isTrue(); - assertThat(this.context.containsBean("bar.errors.bridge")).isTrue(); - producerBinding.unbind(); - assertThat(this.context.containsBean("bar.errors")).isFalse(); - assertThat(this.context.containsBean("bar.errors.bridge")).isFalse(); - } - - @Test - @SuppressWarnings("unchecked") - public void testEndpointBinderHasRecoverer() throws Exception { - // @checkstyle:off - ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration()) - .web(WebApplicationType.NONE).run(); - - AbstractMessageChannelBinder> binder = context - .getBean(AbstractMessageChannelBinder.class); - // @checkstyle:on - - Binding consumerBinding = binder.bindConsumer("foo", "fooGroup", - new DirectChannel(), new ConsumerProperties()); - DirectFieldAccessor consumerBindingAccessor = new DirectFieldAccessor( - consumerBinding); - SubscribableChannel errorChannel = (SubscribableChannel) consumerBindingAccessor - .getPropertyValue("lifecycle.errorChannel"); - assertThat(errorChannel).isNull(); - errorChannel = (SubscribableChannel) consumerBindingAccessor - .getPropertyValue("lifecycle.recoveryCallback.channel"); - assertThat(errorChannel).isNotNull(); - Set handlers = TestUtils.getPropertyValue(errorChannel, - "dispatcher.handlers", Set.class); - assertThat(handlers.size()).isEqualTo(2); - Iterator iterator = handlers.iterator(); - assertThat(iterator.next()).isInstanceOf(BridgeHandler.class); - assertThat(iterator.next()).isInstanceOf(LastSubscriberMessageHandler.class); - assertThat(context.containsBean("foo.fooGroup.errors")).isTrue(); - assertThat(context.containsBean("foo.fooGroup.errors.recoverer")).isTrue(); - assertThat(context.containsBean("foo.fooGroup.errors.handler")).isTrue(); - assertThat(context.containsBean("foo.fooGroup.errors.bridge")).isTrue(); - consumerBinding.unbind(); - assertThat(context.containsBean("foo.fooGroup.errors")).isFalse(); - assertThat(context.containsBean("foo.fooGroup.errors.recoverer")).isFalse(); - assertThat(context.containsBean("foo.fooGroup.errors.handler")).isFalse(); - assertThat(context.containsBean("foo.fooGroup.errors.bridge")).isFalse(); - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/BinderErrorChannelTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/BinderErrorChannelTests.java deleted file mode 100644 index 727b46186..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/BinderErrorChannelTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.integration.dispatcher.AbstractDispatcher; -import org.springframework.messaging.MessageDeliveryException; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Anshul Mehra - */ -public class BinderErrorChannelTests { - - private static final LastSubscriberMessageHandler FINAL_HANDLER = message -> { - - }; - - private static final MessageHandler FIRST_HANDLER = message -> { - - }; - - private static final MessageHandler SECOND_HANDLER = message -> { - - }; - - @Test - void testExceptionIsThrownWhenNoSubscribers() { - BinderErrorChannel channel = new BinderErrorChannel(); - Assertions.assertThrows(MessageDeliveryException.class, () -> { - channel.send(new GenericMessage("hello")); - }); - } - - @Test - void testSubscribeUnsubscribe() { - BinderErrorChannel channel = new BinderErrorChannel(); - DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(channel); - AbstractDispatcher dispatcher = (AbstractDispatcher) fieldAccessor - .getPropertyValue("dispatcher"); - - assertThat(dispatcher).isNotNull(); - assertThat(channel.subscribers()).isEqualTo(0); - assertThat(dispatcher.getHandlerCount()).isEqualTo(0); - - channel.subscribe(FINAL_HANDLER); - - assertThat(channel.subscribers()).isEqualTo(1); - assertThat(dispatcher.getHandlerCount()).isEqualTo(1); - - channel.subscribe(FIRST_HANDLER); - - assertThat(channel.subscribers()).isEqualTo(2); - assertThat(dispatcher.getHandlerCount()).isEqualTo(2); - - channel.subscribe(SECOND_HANDLER); - - assertThat(channel.subscribers()).isEqualTo(3); - assertThat(dispatcher.getHandlerCount()).isEqualTo(3); - - channel.unsubscribe(FIRST_HANDLER); - - assertThat(channel.subscribers()).isEqualTo(2); - assertThat(dispatcher.getHandlerCount()).isEqualTo(2); - - channel.unsubscribe(FINAL_HANDLER); - - assertThat(channel.subscribers()).isEqualTo(1); - assertThat(dispatcher.getHandlerCount()).isEqualTo(1); - - channel.unsubscribe(SECOND_HANDLER); - - assertThat(channel.subscribers()).isEqualTo(0); - assertThat(dispatcher.getHandlerCount()).isEqualTo(0); - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/ErrorBindingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/ErrorBindingTests.java deleted file mode 100644 index daf066374..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/ErrorBindingTests.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -import java.util.function.Consumer; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public class ErrorBindingTests { - - @Test - public void testConfigurationWithDefaultErrorHandler() { - ApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ErrorBindingTests.ErrorConfigurationDefault.class)) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1", - "--spring.cloud.function.definition=handle", - "--spring.cloud.stream.default.error-handler-definition=errorHandler", - "--spring.jmx.enabled=false"); - - InputDestination source = context.getBean(InputDestination.class); - source.send(new GenericMessage("Hello".getBytes())); - source.send(new GenericMessage("Hello".getBytes())); - source.send(new GenericMessage("Hello".getBytes())); - - ErrorConfigurationDefault errorConfiguration = context - .getBean(ErrorConfigurationDefault.class); - assertThat(errorConfiguration.counter).isEqualTo(6); - } - - @Test - void testConfigurationWithBindingSpecificErrorHandler() { - ApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ErrorBindingTests.ErrorConfigurationWithCustomErrorHandler.class)) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1", - "--spring.cloud.function.definition=handle", - "--spring.cloud.stream.bindings.handle-in-0.error-handler-definition=errorHandler", - "--spring.jmx.enabled=false"); - - InputDestination source = context.getBean(InputDestination.class); - source.send(new GenericMessage("Hello".getBytes())); - source.send(new GenericMessage("Hello".getBytes())); - source.send(new GenericMessage("Hello".getBytes())); - - ErrorConfigurationWithCustomErrorHandler errorConfiguration = context - .getBean(ErrorConfigurationWithCustomErrorHandler.class); - assertThat(errorConfiguration.counter).isEqualTo(6); - } - - @Test - void testConfigurationWithoutBinderSpecificErrorHandler() { - ApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoErrorHandler.class)) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.process-in-0.consumer.max-attempts=1", - "--spring.cloud.function.definition=process", - "--spring.jmx.enabled=false"); - - InputDestination source = context.getBean(InputDestination.class); - source.send(new GenericMessage("Hello".getBytes())); - - TestChannelBinder binder = context.getBean(TestChannelBinder.class); - assertThat(binder.getLastError()).isNotNull(); - } - - @Test - void testErrorBindingWithMultipleDestinationPerBinding() { - new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoErrorHandler.class)) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.process-in-0.consumer.max-attempts=1", - "--spring.cloud.function.definition=process", - "--spring.cloud.stream.bindings.process-in-0.destination=one,two", - "--spring.jmx.enabled=false"); - - // must not fail GH-2599 - } - - @EnableAutoConfiguration - public static class TestProcessor { - - @Bean - public Function processor() { - return s -> s; - } - } - - @EnableAutoConfiguration - public static class NoErrorHandler { - - @Bean - public Function process() { - return s -> { - throw new RuntimeException("intentional"); - }; - } - } - - @EnableAutoConfiguration - public static class ErrorConfigurationDefault { - - private int counter; - - @Bean - public Function handle() { - return v -> { - this.counter++; - throw new RuntimeException("Intentional"); - }; - } - - @Bean - public Consumer errorHandler() { - return v -> { - this.counter++; - }; - } - - } - - @EnableAutoConfiguration - public static class ErrorConfigurationWithCustomErrorHandler { - - private int counter; - - @Bean - public Function handle() { - return v -> { - this.counter++; - throw new RuntimeException("Intentional"); - }; - } - - @Bean - public Consumer errorHandler() { - return v -> { - this.counter++; - }; - } - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/PollableConsumerTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/PollableConsumerTests.java deleted file mode 100644 index 9c2ecb2c0..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/PollableConsumerTests.java +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.TestChannelBinder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.binding.MessageConverterConfigurer; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.context.Lifecycle; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.acks.AcknowledgmentCallback; -import org.springframework.integration.acks.AcknowledgmentCallback.Status; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.core.MessageSource; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.converter.SmartMessageConverter; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.GenericMessage; -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.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * @author Gary Russell - * @author Oleg Zhurakousky - * @author David Turanski - * @since 2.0 - * - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class PollableConsumerTests { - - private ApplicationContext context; - - private SmartMessageConverter messageConverter; - - @BeforeAll - public void before() { - this.messageConverter = new CompositeMessageConverterFactory() - .getMessageConverterForAllRegistered(); - } - - @Test - void testDefaultMessageSource() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - AtomicInteger count = new AtomicInteger(); - assertThat(pollableSource.poll(message -> { - assertThat(message).isNotNull(); - count.incrementAndGet(); - })).isTrue(); - assertThat(count.get()).isOne(); - } - - @Test - void testSimple() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder - .withPayload(((String) message.getPayload()).toUpperCase()) - .copyHeaders(message.getHeaders()).build(); - } - - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final AtomicInteger count = new AtomicInteger(); - assertThat(pollableSource.poll(received -> { - assertThat(received.getPayload()).isEqualTo("POLLED DATA"); - assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeType.valueOf("text/plain")); - if (count.incrementAndGet() == 1) { - throw new RuntimeException("test retry"); - } - })).isTrue(); - assertThat(count.get()).isEqualTo(2); - } - - @Test - void testConvertSimple() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - binder.setMessageSourceDelegate( - () -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes())); - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(1); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final AtomicReference payload = new AtomicReference<>(); - assertThat(pollableSource.poll(received -> { - payload.set(received.getPayload()); - }, new ParameterizedTypeReference() { - })).isTrue(); - assertThat(payload.get()).isInstanceOf(Foo.class); - assertThat(((Foo) payload.get()).getFoo()).isEqualTo("bar"); - // test the cache for coverage - assertThat(pollableSource.poll(received -> { - payload.set(received.getPayload()); - }, new ParameterizedTypeReference() { - })).isTrue(); - assertThat(payload.get()).isInstanceOf(Foo.class); - assertThat(((Foo) payload.get()).getFoo()).isEqualTo("bar"); - } - - @Test - void testConvertSimpler() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - BindingServiceProperties bsps = this.context - .getBean(BindingServiceProperties.class); - BindingProperties props = new BindingProperties(); - props.setContentType("text/plain"); - bsps.setBindings(Collections.singletonMap("foo", props)); - - binder.setMessageSourceDelegate(() -> new GenericMessage<>("foo".getBytes())); - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(1); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final AtomicReference payload = new AtomicReference<>(); - assertThat(pollableSource.poll(received -> { - payload.set(received.getPayload()); - }, new ParameterizedTypeReference() { - })).isTrue(); - assertThat(payload.get()).isInstanceOf(String.class); - assertThat(payload.get()).isEqualTo("foo"); - // test the cache for coverage - assertThat(pollableSource.poll(received -> { - payload.set(received.getPayload()); - }, new ParameterizedTypeReference() { - })).isTrue(); - assertThat(payload.get()).isInstanceOf(String.class); - assertThat(payload.get()).isEqualTo("foo"); - } - - @Test -// @Disabled - void testConvertList() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - binder.setMessageSourceDelegate(() -> new GenericMessage<>( - "[{\"foo\":\"bar\"},{\"foo\":\"baz\"}]".getBytes())); - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(1); - properties.setBackOffInitialInterval(0); - - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - - final AtomicReference payload = new AtomicReference<>(); - assertThat(pollableSource.poll(received -> { - payload.set(received.getPayload()); - }, new ParameterizedTypeReference>() { - })).isTrue(); - @SuppressWarnings("unchecked") - List list = (List) payload.get(); - assertThat(list.size()).isEqualTo(2); - assertThat(list.get(0).getFoo()).isEqualTo("bar"); - assertThat(list.get(1).getFoo()).isEqualTo("baz"); - } - - @Test -// @Disabled - void testConvertMap() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - binder.setMessageSourceDelegate( - () -> new GenericMessage<>("{\"qux\":{\"foo\":\"bar\"}}".getBytes())); - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(1); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final AtomicReference payload = new AtomicReference<>(); - assertThat(pollableSource.poll(received -> { - payload.set(received.getPayload()); - }, new ParameterizedTypeReference>() { - })).isTrue(); - @SuppressWarnings("unchecked") - Map map = (Map) payload.get(); - assertThat(map.size()).isEqualTo(1); - assertThat(map.get("qux").getFoo()).isEqualTo("bar"); - } - - @Test - void testEmbedded() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - binder.setMessageSourceDelegate(() -> { - MessageValues original = new MessageValues("foo".getBytes(), - Collections.singletonMap(MessageHeaders.CONTENT_TYPE, - "application/octet-stream")); - byte[] payload = new byte[0]; - try { - payload = EmbeddedHeaderUtils.embedHeaders(original, - MessageHeaders.CONTENT_TYPE); - } - catch (Exception e) { - fail(e.getMessage()); - } - return new GenericMessage<>(payload); - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setHeaderMode(HeaderMode.embeddedHeaders); - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder - .withPayload( - new String((byte[]) message.getPayload()).toUpperCase()) - .copyHeaders(message.getHeaders()).build(); - } - - }); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - assertThat(pollableSource.poll(received -> { - assertThat(received.getPayload()).isEqualTo("FOO"); - assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo("application/octet-stream"); - })).isTrue(); - } - - @Test - void testErrors() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder - .withPayload(((String) message.getPayload()).toUpperCase()) - .copyHeaders(message.getHeaders()).build(); - } - - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - properties.getRetryableExceptions().put(IllegalStateException.class, false); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final CountDownLatch latch = new CountDownLatch(1); - this.context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, - SubscribableChannel.class).subscribe(m -> { - latch.countDown(); - }); - final AtomicInteger count = new AtomicInteger(); - assertThat(pollableSource.poll(received -> { - count.incrementAndGet(); - throw new RuntimeException("test recoverer"); - })).isTrue(); - assertThat(count.get()).isEqualTo(2); - Message lastError = binder.getLastError(); - assertThat(lastError).isNotNull(); - assertThat(((Exception) lastError.getPayload()).getCause().getMessage()) - .isEqualTo("test recoverer"); - assertThat(pollableSource.poll(received -> { - count.incrementAndGet(); - throw new IllegalStateException("no retries"); - })).isTrue(); - assertThat(count.get()).isEqualTo(3); - lastError = binder.getLastError(); - assertThat(lastError).isNotNull(); - assertThat(((Exception) lastError.getPayload()).getCause().getMessage()) - .isEqualTo("no retries"); - } - - @Test - void testErrorsNoRetry() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder - .withPayload(((String) message.getPayload()).toUpperCase()) - .copyHeaders(message.getHeaders()).build(); - } - - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setMaxAttempts(1); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final CountDownLatch latch = new CountDownLatch(1); - this.context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, - SubscribableChannel.class).subscribe(m -> { - latch.countDown(); - }); - final AtomicInteger count = new AtomicInteger(); - assertThat(pollableSource.poll(received -> { - count.incrementAndGet(); - throw new RuntimeException("test recoverer"); - })).isTrue(); - assertThat(count.get()).isEqualTo(1); - } - - @Test - void testRequeue() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - AcknowledgmentCallback callback = mock(AcknowledgmentCallback.class); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder.fromMessage(message) - .setHeader( - IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, - callback) - .build(); - } - - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>(null); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final AtomicInteger count = new AtomicInteger(); - try { - assertThat(pollableSource.poll(received -> { - count.incrementAndGet(); - throw new RequeueCurrentMessageException("test retry"); - })).isTrue(); - } - catch (Exception e) { - // no op - } - assertThat(count.get()).isEqualTo(2); - verify(callback).acknowledge(Status.REQUEUE); - } - - @Test - void testRequeueWithNoAcknowledgementCallback() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder.fromMessage(message) - .build(); - } - - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>(null); - properties.setMaxAttempts(2); - properties.setBackOffInitialInterval(0); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - final AtomicInteger count = new AtomicInteger(); - - assertThat(pollableSource.poll(received -> { - count.incrementAndGet(); - throw new RequeueCurrentMessageException("test retry"); - })).isTrue(); - - assertThat(count.get()).isEqualTo(2); - - } - - @Test - void testRequeueFromErrorFlow() { - TestChannelBinder binder = createBinder(); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - AcknowledgmentCallback callback = mock(AcknowledgmentCallback.class); - pollableSource.addInterceptor(new ChannelInterceptor() { - - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder.fromMessage(message) - .setHeader( - IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, - callback) - .build(); - } - - }); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>(null); - properties.setMaxAttempts(1); - binder.bindPollableConsumer("foo", "bar", pollableSource, properties); - SubscribableChannel errorChannel = new DirectChannel(); - errorChannel.subscribe(msg -> { - throw new RequeueCurrentMessageException((Throwable) msg.getPayload()); - }); - pollableSource.setErrorChannel(errorChannel); - try { - pollableSource.poll(received -> { - throw new RuntimeException("test requeue from error flow"); - }); - } - catch (Exception e) { - // no op - } - verify(callback).acknowledge(Status.REQUEUE); - } - - @SuppressWarnings("unchecked") - @Test - void testAutoStartupOff() { - TestChannelBinder binder = createBinder(); - binder.setMessageSourceDelegate(new LifecycleMessageSource( - () -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes()))); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setAutoStartup(false); - - Binding> pollableSourceBinding = binder - .bindPollableConsumer("foo", "bar", pollableSource, properties); - - assertThat(pollableSourceBinding.isRunning()).isFalse(); - } - - @SuppressWarnings("unchecked") - @Test - void testAutoStartupOn() { - TestChannelBinder binder = createBinder(); - binder.setMessageSourceDelegate(new LifecycleMessageSource( - () -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes()))); - MessageConverterConfigurer configurer = this.context - .getBean(MessageConverterConfigurer.class); - - DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource( - this.messageConverter); - configurer.configurePolledMessageSource(pollableSource, "foo"); - ExtendedConsumerProperties properties = new ExtendedConsumerProperties<>( - null); - properties.setAutoStartup(true); - - Binding> pollableSourceBinding = binder - .bindPollableConsumer("foo", "bar", pollableSource, properties); - - assertThat(pollableSourceBinding.isRunning()).isTrue(); - } - - private TestChannelBinder createBinder(String... args) { - this.context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration()) - .web(WebApplicationType.NONE).run(args); - TestChannelBinder binder = this.context.getBean(TestChannelBinder.class); - return binder; - } - - public static class Foo { - - private String foo; - - protected String getFoo() { - return this.foo; - } - - protected void setFoo(String foo) { - this.foo = foo; - } - - } - - public static class LifecycleMessageSource implements MessageSource, Lifecycle { - private final MessageSource delegate; - - private boolean running = false; - - public LifecycleMessageSource(MessageSource delegate) { - this.delegate = delegate; - } - - @Override - public Message receive() { - return this.delegate.receive(); - } - - @Override - public void start() { - this.running = true; - } - - @Override - public void stop() { - this.running = false; - } - - @Override - public boolean isRunning() { - return this.running; - } - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/tck/ContentTypeTckTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/tck/ContentTypeTckTests.java deleted file mode 100644 index cf330201c..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binder/tck/ContentTypeTckTests.java +++ /dev/null @@ -1,831 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.tck; - -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.function.json.JsonMapper; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -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.AbstractMessageConverter; -import org.springframework.messaging.converter.MessageConversionException; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Sort of a TCK test suite to validate payload conversion is done properly by interacting - * with binder's input/output destinations instead of its bridged channels. This means - * that all payloads (sent/received) must be expressed in the wire format (byte[]) - * - * @author Oleg Zhurakousky - * @author Gary Russell - * @author Byungjun You - * - */ -public class ContentTypeTckTests { - - @Test - void stringToMapMessageStreamListener() { - ApplicationContext context = new SpringApplicationBuilder( - StringToMapMessageConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload())).isEqualTo("oleg"); - } - - @Test - void pojoToPojo() { - ApplicationContext context = new SpringApplicationBuilder( - PojoToPojoConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void pojoToString() { - ApplicationContext context = new SpringApplicationBuilder( - PojoToStringConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("oleg"); - } - - @Test - void pojoToStringOutboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - PojoToStringConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.echo-out-0.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("oleg"); - } - - @Test - void pojoToByteArray() { - ApplicationContext context = new SpringApplicationBuilder( - PojoToByteArrayConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("oleg"); - } - - @Test - void pojoToByteArrayOutboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - PojoToByteArrayConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("oleg"); - } - - @Test - void stringToPojoInboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - StringToPojoConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.echo-on-0.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void typelessToPojoInboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessToPojoConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void typelessToPojoInboundContentTypeBindingJson() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessToPojoConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.echo-in-0.contentType=application/json", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void typelessMessageToPojoInboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessMessageToPojoConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void typelessMessageToPojoInboundContentTypeBindingJson() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessMessageToPojoConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.echo-in-0.contentType=application/json", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void typelessToPojoWithTextHeaderContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessToPojoConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(MessageBuilder.withPayload(jsonPayload.getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeType.valueOf("text/plain")) - .build()); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void typelessToPojoOutboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessToMessageConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.echo-out-0.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(MessageBuilder.withPayload(jsonPayload.getBytes()) - .setHeader("contentType", new MimeType("text", "plain")).build()); - - Message outputMessage = target.receive(); - System.out.println(new String(outputMessage.getPayload())); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE); - assertThat(outputMessage.getPayload()) - .isEqualTo(jsonPayload.getBytes()); - } - - @Test - void outboundMessageWithTextContentTypeOnly() { - ApplicationContext context = new SpringApplicationBuilder( - TypelessToMessageTextOnlyContentTypeConfiguration.class) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(MessageBuilder.withPayload(jsonPayload.getBytes()) - .setHeader("contentType", new MimeType("text")).build()); - - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString()) - .startsWith("text/"); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void stringToPojoInboundContentTypeHeader() { - ApplicationContext context = new SpringApplicationBuilder( - StringToPojoConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes(), - new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, - MimeTypeUtils.TEXT_PLAIN)))); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void byteArrayToPojoInboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - ByteArrayToPojoConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void byteArrayToPojoInboundContentTypeHeader() { - ApplicationContext context = new SpringApplicationBuilder( - StringToPojoConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes(), - new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, - MimeTypeUtils.TEXT_PLAIN)))); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void byteArrayToByteArray() { - ApplicationContext context = new SpringApplicationBuilder( - ByteArrayToByteArrayConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void byteArrayToByteArrayInboundOutboundContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - ByteArrayToByteArrayConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.cloud.stream.bindings.input.contentType=text/plain", - "--spring.cloud.stream.bindings.output.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void pojoMessageToStringMessage() { - ApplicationContext context = new SpringApplicationBuilder( - PojoMessageToStringMessageConfiguration.class) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("oleg"); - } - - @Test - void customMessageConverter_defaultContentTypeBinding() { - ApplicationContext context = new SpringApplicationBuilder( - StringToStringConfiguration.class, CustomConverters.class) - .web(WebApplicationType.NONE) - .run("--spring.cloud.stream.default.contentType=foo/bar", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage).isNotNull(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo("FooBarMessageConverter"); - assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo("foo/bar"); - } - - // Failure tests - - @Test - void _jsonToPojoWrongDefaultContentTypeProperty() { - ApplicationContext context = new SpringApplicationBuilder( - PojoToPojoConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.default.contentType=text/plain", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - TestChannelBinder binder = context.getBean(TestChannelBinder.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - assertThat(binder.getLastError().getPayload() instanceof MessagingException) - .isTrue(); - } - - @Test - @Disabled - void _toStringDefaultContentTypePropertyUnknownContentType() { - ApplicationContext context = new SpringApplicationBuilder( - StringToStringConfiguration.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.default.contentType=foo/bar", - "--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - TestChannelBinder binder = context.getBean(TestChannelBinder.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - assertThat( - binder.getLastError().getPayload() instanceof MessageConversionException) - .isTrue(); - } - - @Test - void toCollectionWithParameterizedType() { - ApplicationContext context = new SpringApplicationBuilder( - CollectionWithParameterizedTypes.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "[{\"person\":{\"name\":\"jon\"},\"id\":123},{\"person\":{\"name\":\"jane\"},\"id\":456}]"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(outputMessage.getPayload()).isEqualTo(jsonPayload.getBytes()); - } - - // ====== - @Test - void testWithMapInputParameter() { - ApplicationContext context = new SpringApplicationBuilder( - MapInputConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void testWithMapPayloadParameter() { - ApplicationContext context = new SpringApplicationBuilder( - MapInputConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - void testWithListInputParameter() { - ApplicationContext context = new SpringApplicationBuilder( - ListInputConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--debug"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "[\"foo\",\"bar\"]"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isEqualTo(jsonPayload); - } - - @Test - @Disabled // TODO fix it. We can recognize MessageHeaders and parse it out of the message properly - void testWithMessageHeadersInputParameter() { - ApplicationContext context = new SpringApplicationBuilder( - MessageHeadersInputConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - InputDestination source = context.getBean(InputDestination.class); - OutputDestination target = context.getBean(OutputDestination.class); - String jsonPayload = "{\"name\":\"oleg\"}"; - source.send(new GenericMessage<>(jsonPayload.getBytes())); - Message outputMessage = target.receive(); - assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8)) - .isNotEqualTo(jsonPayload); - assertThat(outputMessage.getHeaders().containsKey(MessageHeaders.ID)).isTrue(); - assertThat(outputMessage.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) - .isTrue(); - } - - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class CollectionWithParameterizedTypes { - - @Bean - public Function>, List>> echo() { - return value -> { - assertThat(value.get(0) != null).isTrue(); - return value; - }; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class PojoToPojoConfiguration { - - @Bean - public Function ecgo() { - return value -> value; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class PojoToStringConfiguration { - - @Bean - public Function echo() { - return value -> value.toString(); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class PojoToByteArrayConfiguration { - - @Bean - public Function echo() { - return value -> value.toString().getBytes(StandardCharsets.UTF_8); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class ByteArrayToPojoConfiguration { - - @Bean - public Function echo(JsonMapper mapper) { - return value -> mapper.fromJson(value, Person.class); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class StringToPojoConfiguration { - - @Bean - public Function echo(JsonMapper mapper) { - return value -> mapper.fromJson(value, Person.class); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class TypelessToPojoConfiguration { - - @Bean - public Function echo(JsonMapper mapper) { - return value -> value instanceof byte[] valueAsBytes - ? mapper.fromJson(valueAsBytes, Person.class) - : mapper.fromJson((String) value, Person.class); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class TypelessMessageToPojoConfiguration { - - @Bean - public Function, Person> echo(JsonMapper mapper) { - return message -> message.getPayload() instanceof byte[] payloadAsBytes - ? mapper.fromJson(payloadAsBytes, Person.class) - : mapper.fromJson((String) message.getPayload(), Person.class); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class TypelessToMessageConfiguration { - - @Bean - public Function> echo() { - return value -> MessageBuilder.withPayload(value.toString()) - .setHeader("contentType", new MimeType("text", "plain")).build(); - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class TypelessToMessageTextOnlyContentTypeConfiguration { - - @Bean - public Function> echo() { - return value -> { - return MessageBuilder.withPayload(value.toString()) - .setHeader("expected-content-type", "text/*").build(); - }; - } - } - - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class ByteArrayToByteArrayConfiguration { - - @Bean - public Function echo() { - return value -> value; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class StringToStringConfiguration { - - @Bean - public Function echo() { - return v -> v; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class StringToMapMessageConfiguration { - - @Bean - public Function>, String> echo() { - return value -> { - assertThat(value.getPayload() instanceof Map).isTrue(); - return (String) value.getPayload().get("name"); - }; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class PojoMessageToStringMessageConfiguration { - - @Bean - public Function, Message> echo() { - return value -> MessageBuilder.withPayload(value.getPayload().toString()) - .setHeader("expected-content-type", MimeTypeUtils.TEXT_PLAIN_VALUE) - .build(); - } - } - - public static class Employee

{ - - private P person; - - private int id; - - public int getId() { - return this.id; - } - - public void setId(int id) { - this.id = id; - } - - public P getPerson() { - return this.person; - } - - public void setPerson(P person) { - this.person = person; - } - - } - - public static class Person { - - private String name; - - public Person() { - this(null); - } - - public Person(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return this.name; - } - - } - - @Configuration(proxyBeanMethods = false) - public static class CustomConverters { - - @Bean - public FooBarMessageConverter fooBarMessageConverter() { - return new FooBarMessageConverter(MimeType.valueOf("foo/bar")); - } - - @Bean - public AlwaysStringKryoMessageConverter kryoOverrideMessageConverter() { - return new AlwaysStringKryoMessageConverter( - MimeType.valueOf("application/x-java-object")); - } - - /** - * Even though this MessageConverter has nothing to do with Kryo it still shows - * how Kryo conversion can be customized/overriden since it simply overriding a - * converter for contentType 'application/x-java-object'. - * - */ - public static class AlwaysStringKryoMessageConverter - extends AbstractMessageConverter { - - public AlwaysStringKryoMessageConverter(MimeType supportedMimeType) { - super(supportedMimeType); - } - - @Override - protected boolean supports(Class clazz) { - return clazz == null || String.class.isAssignableFrom(clazz); - } - - @Override - protected Object convertFromInternal(Message message, Class targetClass, - @Nullable Object conversionHint) { - return this.getClass().getSimpleName(); - } - - @Override - protected Object convertToInternal(Object payload, - @Nullable MessageHeaders headers, @Nullable Object conversionHint) { - return ((String) payload).getBytes(StandardCharsets.UTF_8); - } - - } - - public static class FooBarMessageConverter extends AbstractMessageConverter { - - protected FooBarMessageConverter(MimeType supportedMimeType) { - super(supportedMimeType); - } - - @Override - protected boolean supports(Class clazz) { - return clazz != null && String.class.isAssignableFrom(clazz); - } - - @Override - protected Object convertFromInternal(Message message, Class targetClass, - @Nullable Object conversionHint) { - return this.getClass().getSimpleName(); - } - - @Override - protected Object convertToInternal(Object payload, - @Nullable MessageHeaders headers, @Nullable Object conversionHint) { - return ((String) payload).getBytes(StandardCharsets.UTF_8); - } - - } - - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class MapInputConfiguration { - - @Bean - public Function, Map> echo() { - return value -> value; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class ListInputConfiguration { - @Bean - public Function, List> echo() { - return v -> v; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class MessageHeadersInputConfiguration { - - @Bean - public Function> echo() { - return v -> v; - } - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/BindingServiceTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/BindingServiceTests.java deleted file mode 100644 index 2370d04be..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/BindingServiceTests.java +++ /dev/null @@ -1,645 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderChildContextInitializer; -import org.springframework.cloud.stream.binder.BinderConfiguration; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.BinderType; -import org.springframework.cloud.stream.binder.BinderTypeRegistry; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.DefaultBinderFactory; -import org.springframework.cloud.stream.binder.DefaultBinderTypeRegistry; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.reflection.GenericsUtils; -import org.springframework.cloud.stream.utils.IntegrationTestsMockBinderConfiguration; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.util.ReflectionUtils; - -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.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * @author Gary Russell - * @author Mark Fisher - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Janne Valkealahti - * @author Soby Chacko - * @author Michael Michailidis - * @author Chris Bono - * @author Artem Bilan - */ -public class BindingServiceTests { - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Test - void testDefaultGroup() throws Exception { - BindingServiceProperties properties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - props.setDestination("foo"); - final String inputChannelName = "input"; - bindingProperties.put(inputChannelName, props); - properties.setBindings(bindingProperties); - DefaultBinderFactory binderFactory = createMockBinderFactory(); - Binder binder = binderFactory.getBinder("mock", MessageChannel.class); - BindingService service = new BindingService(properties, binderFactory, new ObjectMapper()); - MessageChannel inputChannel = new DirectChannel(); - Binding mockBinding = Mockito.mock(Binding.class); - when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel), - any(ConsumerProperties.class))).thenReturn(mockBinding); - Collection> bindings = service.bindConsumer(inputChannel, - inputChannelName); - assertThat(bindings).hasSize(1); - Binding binding = bindings.iterator().next(); - assertThat(binding).isSameAs(mockBinding); - service.unbindConsumers(inputChannelName); - verify(binder).bindConsumer(eq("foo"), isNull(), same(inputChannel), - any(ConsumerProperties.class)); - verify(binding).unbind(); - binderFactory.destroy(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Test - void testMultipleConsumerBindings() { - BindingServiceProperties properties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - props.setDestination("foo,bar"); - String inputChannelName = "input"; - bindingProperties.put(inputChannelName, props); - - properties.setBindings(bindingProperties); - - DefaultBinderFactory binderFactory = createMockBinderFactory(); - - Binder binder = binderFactory.getBinder("mock", MessageChannel.class); - BindingService service = new BindingService(properties, binderFactory, new ObjectMapper()); - MessageChannel inputChannel = new DirectChannel(); - - Binding mockBinding1 = Mockito.mock(Binding.class); - Binding mockBinding2 = Mockito.mock(Binding.class); - - when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel), - any(ConsumerProperties.class))).thenReturn(mockBinding1); - when(binder.bindConsumer(eq("bar"), isNull(), same(inputChannel), - any(ConsumerProperties.class))).thenReturn(mockBinding2); - - Collection> bindings = service.bindConsumer(inputChannel, - "input"); - assertThat(bindings).hasSize(2); - - Iterator> iterator = bindings.iterator(); - Binding binding1 = iterator.next(); - Binding binding2 = iterator.next(); - - assertThat(binding1).isSameAs(mockBinding1); - assertThat(binding2).isSameAs(mockBinding2); - - assertThat(service.getConsumerBindings("input")).containsSequence(bindings); - - service.unbindConsumers("input"); - - verify(binder).bindConsumer(eq("foo"), isNull(), same(inputChannel), - any(ConsumerProperties.class)); - verify(binder).bindConsumer(eq("bar"), isNull(), same(inputChannel), - any(ConsumerProperties.class)); - verify(binding1).unbind(); - verify(binding2).unbind(); - - binderFactory.destroy(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Test - void testMultipleConsumerBindingsFromIndexList() throws Exception { - BindingServiceProperties properties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - props.setDestination("foo"); - - ConsumerProperties consumer = properties.getConsumerProperties("input"); - consumer.setInstanceIndexList(Arrays.asList(0, 1)); - consumer.setInstanceCount(2); - consumer.setPartitioned(true); - props.setConsumer(consumer); - - final String inputChannelName = "input"; - bindingProperties.put(inputChannelName, props); - - properties.setBindings(bindingProperties); - - DefaultBinderFactory binderFactory = createMockBinderFactory(); - - Binder binder = binderFactory.getBinder("mock", MessageChannel.class); - BindingService service = new BindingService(properties, binderFactory, new ObjectMapper()); - MessageChannel inputChannel = new DirectChannel(); - - Binding mockBinding1 = Mockito.mock(Binding.class, "FirstBinding"); - Binding mockBinding2 = Mockito.mock(Binding.class, "SecondBinding"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(ConsumerProperties.class); - - when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel), - any(ConsumerProperties.class))).thenReturn(mockBinding1).thenReturn(mockBinding2); - - Collection> bindings = service.bindConsumer(inputChannel, - "input"); - assertThat(bindings).hasSize(2); - - Iterator> iterator = bindings.iterator(); - Binding binding1 = iterator.next(); - Binding binding2 = iterator.next(); - - assertThat(binding1).isSameAs(mockBinding1); - assertThat(binding2).isSameAs(mockBinding2); - - service.unbindConsumers("input"); - - verify(binder, times(2)).bindConsumer(eq("foo"), isNull(), same(inputChannel), - captor.capture()); - verify(binding1).unbind(); - verify(binding2).unbind(); - - List allValues = captor.getAllValues(); - - assertThat(allValues.size()).isEqualTo(2); - - assertThat(allValues.get(0).getInstanceIndex()).isEqualTo(0); - assertThat(allValues.get(1).getInstanceIndex()).isEqualTo(1); - - binderFactory.destroy(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Test - void testConsumerBindingWhenMultiplexingIsEnabled() throws Exception { - BindingServiceProperties properties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - props.setDestination("foo,bar"); - - ConsumerProperties consumer = properties.getConsumerProperties("input"); - consumer.setMultiplex(true); - props.setConsumer(consumer); - - final String inputChannelName = "input"; - bindingProperties.put(inputChannelName, props); - - properties.setBindings(bindingProperties); - - DefaultBinderFactory binderFactory = createMockBinderFactory(); - - Binder binder = binderFactory.getBinder("mock", MessageChannel.class); - BindingService service = new BindingService(properties, binderFactory, new ObjectMapper()); - MessageChannel inputChannel = new DirectChannel(); - - Binding mockBinding1 = Mockito.mock(Binding.class); - - when(binder.bindConsumer(eq("foo,bar"), isNull(), same(inputChannel), - any(ConsumerProperties.class))).thenReturn(mockBinding1); - - Collection> bindings = service.bindConsumer(inputChannel, - "input"); - assertThat(bindings).hasSize(1); - - Iterator> iterator = bindings.iterator(); - Binding binding1 = iterator.next(); - - assertThat(binding1).isSameAs(mockBinding1); - - service.unbindConsumers("input"); - - verify(binder).bindConsumer(eq("foo,bar"), isNull(), same(inputChannel), - any(ConsumerProperties.class)); - verify(binding1).unbind(); - - binderFactory.destroy(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Test - void testExplicitGroup() throws Exception { - BindingServiceProperties properties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - props.setDestination("foo"); - props.setGroup("fooGroup"); - final String inputChannelName = "input"; - bindingProperties.put(inputChannelName, props); - properties.setBindings(bindingProperties); - DefaultBinderFactory binderFactory = createMockBinderFactory(); - Binder binder = binderFactory.getBinder("mock", MessageChannel.class); - BindingService service = new BindingService(properties, binderFactory, new ObjectMapper()); - MessageChannel inputChannel = new DirectChannel(); - Binding mockBinding = Mockito.mock(Binding.class); - when(binder.bindConsumer(eq("foo"), eq("fooGroup"), same(inputChannel), - any(ConsumerProperties.class))).thenReturn(mockBinding); - Collection> bindings = service.bindConsumer(inputChannel, - inputChannelName); - assertThat(bindings).hasSize(1); - Binding binding = bindings.iterator().next(); - assertThat(binding).isSameAs(mockBinding); - - service.unbindConsumers(inputChannelName); - verify(binder).bindConsumer(eq("foo"), eq(props.getGroup()), same(inputChannel), - any(ConsumerProperties.class)); - verify(binding).unbind(); - binderFactory.destroy(); - } - - @Test - @Disabled - void testProducerPropertiesValidation() { - BindingServiceProperties serviceProperties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - ProducerProperties producerProperties = new ProducerProperties(); - producerProperties.setPartitionCount(0); - props.setDestination("foo"); - props.setProducer(producerProperties); - final String outputChannelName = "output"; - bindingProperties.put(outputChannelName, props); - serviceProperties.setBindings(bindingProperties); - DefaultBinderFactory binderFactory = createMockBinderFactory(); - BindingService service = new BindingService(serviceProperties, binderFactory, new ObjectMapper()); - MessageChannel outputChannel = new DirectChannel(); - try { - service.bindProducer(outputChannel, outputChannelName); - fail("Producer properties should be validated."); - } - catch (IllegalStateException e) { - assertThat(e) - .hasMessageContaining("Partition count should be greater than zero."); - } - } - - @Test - void testDefaultPropertyBehavior() { - ConfigurableApplicationContext run = SpringApplication.run( - DefaultConsumerPropertiesTestSink.class, - "--server.port=0", - "--spring.cloud.stream.output-bindings=output1", - "--spring.cloud.stream.input-bindings=inputFooBarBuzz", - "--spring.cloud.stream.default.contentType=text/plain", - "--spring.cloud.stream.bindings.input1.contentType=application/json", - "--spring.cloud.stream.default.group=foo", - "--spring.cloud.stream.bindings.input2.group=bar", - "--spring.cloud.stream.default.consumer.concurrency=5", - "--spring.cloud.stream.bindings.input2.consumer.concurrency=1", - "--spring.cloud.stream.bindings.input1.consumer.partitioned=true", - "--spring.cloud.stream.default.producer.partitionCount=10", - "--spring.cloud.stream.bindings.output2.producer.partitionCount=1", - "--spring.cloud.stream.bindings.inputXyz.contentType=application/json", - "--spring.cloud.stream.bindings.inputFooBar.contentType=application/avro", - "--spring.cloud.stream.bindings.input_snake_case.contentType=application/avro"); - - BindingServiceProperties bindingServiceProperties = run.getBeanFactory() - .getBean(BindingServiceProperties.class); - Map bindings = bindingServiceProperties.getBindings(); - - assertThat(bindings.get("input1").getContentType()).isEqualTo("application/json"); - assertThat(bindings.get("input2").getContentType()).isEqualTo("text/plain"); - assertThat(bindings.get("input1").getGroup()).isEqualTo("foo"); - assertThat(bindings.get("input2").getGroup()).isEqualTo("bar"); - assertThat(bindings.get("input1").getConsumer().getConcurrency()).isEqualTo(5); - assertThat(bindings.get("input2").getConsumer().getConcurrency()).isEqualTo(1); - assertThat(bindings.get("input1").getConsumer().isPartitioned()).isEqualTo(true); - assertThat(bindings.get("input2").getConsumer().isPartitioned()).isEqualTo(false); - assertThat(bindings.get("output1").getProducer().getPartitionCount()) - .isEqualTo(10); - assertThat(bindings.get("output2").getProducer().getPartitionCount()) - .isEqualTo(1); - - assertThat(bindings.get("inputXyz").getContentType()) - .isEqualTo("application/json"); - assertThat(bindings.get("inputFooBar").getContentType()) - .isEqualTo("application/avro"); - assertThat(bindings.get("inputFooBarBuzz").getContentType()) - .isEqualTo("text/plain"); - assertThat(bindings.get("input_snake_case").getContentType()) - .isEqualTo("application/avro"); - - run.close(); - } - - @Test - @Disabled - void testConsumerPropertiesValidation() { - BindingServiceProperties serviceProperties = new BindingServiceProperties(); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - ConsumerProperties consumerProperties = new ConsumerProperties(); - consumerProperties.setConcurrency(0); - props.setDestination("foo"); - props.setConsumer(consumerProperties); - final String inputChannelName = "input"; - bindingProperties.put(inputChannelName, props); - serviceProperties.setBindings(bindingProperties); - DefaultBinderFactory binderFactory = createMockBinderFactory(); - BindingService service = new BindingService(serviceProperties, binderFactory, new ObjectMapper()); - MessageChannel inputChannel = new DirectChannel(); - try { - service.bindConsumer(inputChannel, inputChannelName); - fail("Consumer properties should be validated."); - } - catch (IllegalStateException e) { - assertThat(e) - .hasMessageContaining("Concurrency should be greater than zero."); - } - } - - @Test - void testUnknownBinderOnBindingFailure() { - HashMap properties = new HashMap<>(); - properties.put("spring.cloud.stream.bindings.input.destination", "fooInput"); - properties.put("spring.cloud.stream.bindings.input.binder", "mock"); - properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput"); - properties.put("spring.cloud.stream.bindings.output.binder", "mockError"); - BindingServiceProperties bindingServiceProperties = createBindingServiceProperties( - properties); - BindingService bindingService = new BindingService(bindingServiceProperties, - createMockBinderFactory(), new ObjectMapper()); - bindingService.bindConsumer(new DirectChannel(), "input"); - try { - bindingService.bindProducer(new DirectChannel(), "output"); - fail("Expected 'Unknown binder configuration'"); - } - catch (IllegalStateException e) { - assertThat(e).hasMessageContaining("Unknown binder configuration: mockError"); - } - } - - @SuppressWarnings("unchecked") - @Test - void testUnrecognizedBinderAllowedIfNotUsed() { - HashMap properties = new HashMap<>(); - properties.put("spring.cloud.stream.bindings.input.destination", "fooInput"); - properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput"); - properties.put("spring.cloud.stream.defaultBinder", "mock1"); - properties.put("spring.cloud.stream.binders.mock1.type", "mock"); - properties.put("spring.cloud.stream.binders.kafka1.type", "kafka"); - BindingServiceProperties bindingServiceProperties = createBindingServiceProperties( - properties); - BinderFactory binderFactory = new BindingServiceConfiguration() - .binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class), - Mockito.mock(BinderChildContextInitializer.class)); - BindingService bindingService = new BindingService(bindingServiceProperties, - binderFactory, new ObjectMapper()); - bindingService.bindConsumer(new DirectChannel(), "input"); - bindingService.bindProducer(new DirectChannel(), "output"); - } - - @SuppressWarnings("unchecked") - @Test - void testUnrecognizedBinderDisallowedIfUsed() { - HashMap properties = new HashMap<>(); - properties.put("spring.cloud.stream.bindings.input.destination", "fooInput"); - properties.put("spring.cloud.stream.bindings.input.binder", "mock1"); - properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput"); - properties.put("spring.cloud.stream.bindings.output.type", "kafka1"); - properties.put("spring.cloud.stream.binders.mock1.type", "mock"); - properties.put("spring.cloud.stream.binders.kafka1.type", "kafka"); - BindingServiceProperties bindingServiceProperties = createBindingServiceProperties( - properties); - BinderFactory binderFactory = new BindingServiceConfiguration() - .binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class), - Mockito.mock(BinderChildContextInitializer.class)); - BindingService bindingService = new BindingService(bindingServiceProperties, - binderFactory, new ObjectMapper()); - bindingService.bindConsumer(new DirectChannel(), "input"); - try { - bindingService.bindProducer(new DirectChannel(), "output"); - fail("Expected 'Unknown binder configuration'"); - } - catch (IllegalArgumentException e) { - assertThat(e).hasMessageContaining("Binder type kafka is not defined"); - } - } - - @Test - void testResolveBindableType() { - Class bindableType = GenericsUtils.getParameterType(FooBinder.class, - Binder.class, 0); - assertThat(bindableType).isSameAs(SomeBindableType.class); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testLateBindingProducer() throws Exception { - BindingServiceProperties properties = new BindingServiceProperties(); - properties.setBindingRetryInterval(1); - Map bindingProperties = new HashMap<>(); - BindingProperties props = new BindingProperties(); - props.setDestination("foo"); - final String outputChannelName = "output"; - bindingProperties.put(outputChannelName, props); - properties.setBindings(bindingProperties); - DefaultBinderFactory binderFactory = createMockBinderFactory(); - Binder binder = binderFactory.getBinder("mock", MessageChannel.class); - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.initialize(); - BindingService service = new BindingService(properties, binderFactory, scheduler, new ObjectMapper()); - MessageChannel outputChannel = new DirectChannel(); - final Binding mockBinding = Mockito.mock(Binding.class); - final CountDownLatch fail = new CountDownLatch(2); - doAnswer(i -> { - fail.countDown(); - if (fail.getCount() == 1) { - throw new RuntimeException("fail"); - } - return mockBinding; - }).when(binder).bindProducer(eq("foo"), same(outputChannel), - any(ProducerProperties.class)); - Binding binding = service.bindProducer(outputChannel, - outputChannelName); - assertThat(fail.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(binding).isNotNull(); - Binding delegate = TestUtils.getPropertyValue(binding, "delegate", Binding.class); - int n = 0; - while (n++ < 300 && delegate == null) { - Thread.sleep(100); - delegate = TestUtils.getPropertyValue(binding, "delegate", Binding.class); - } - assertThat(delegate).isSameAs(mockBinding); - - assertThat(service.getProducerBinding("output")).isSameAs(binding); - - service.unbindProducers(outputChannelName); - verify(binder, times(2)).bindProducer(eq("foo"), same(outputChannel), - any(ProducerProperties.class)); - verify(delegate).unbind(); - binderFactory.destroy(); - scheduler.destroy(); - } - - @SuppressWarnings("unchecked") - @Test - void testBindingAutostartup() throws Exception { - ApplicationContext context = new SpringApplicationBuilder(FooConfiguration.class) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.input-in-0.consumer.auto-startup=false"); - BindingService bindingService = context.getBean(BindingService.class); - - Field cbField = ReflectionUtils.findField(BindingService.class, - "consumerBindings"); - cbField.setAccessible(true); - Map cbMap = (Map) cbField.get(bindingService); - Binding inputBinding = ((List>) cbMap.get("input-in-0")).get(0); - assertThat(inputBinding.isRunning()).isFalse(); - } - - @SuppressWarnings("unchecked") - @Test - void testBindingNameAsTopLevelProperty() throws Exception { - ApplicationContext context = new SpringApplicationBuilder(BarConfiguration.class) - .web(WebApplicationType.NONE).run(); - - final BindingServiceProperties bindingServiceProperties = context.getBean(BindingServiceProperties.class); - - final ConsumerProperties consumerProperties = bindingServiceProperties.getConsumerProperties("myFunction-in-0"); - assertThat(consumerProperties.getBindingName()).isEqualTo("myFunction-in-0"); - final ProducerProperties producerProperties = bindingServiceProperties.getProducerProperties("myFunction-out-0"); - assertThat(producerProperties.getBindingName()).isEqualTo("myFunction-out-0"); - } - - private DefaultBinderFactory createMockBinderFactory() { - BinderTypeRegistry binderTypeRegistry = createMockBinderTypeRegistry(); - return new DefaultBinderFactory( - Collections.singletonMap("mock", - new BinderConfiguration("mock", new HashMap<>(), true, true)), - binderTypeRegistry, null); - } - - private DefaultBinderTypeRegistry createMockBinderTypeRegistry() { - return new DefaultBinderTypeRegistry(Collections.singletonMap("mock", - new BinderType("mock", new Class[] { IntegrationTestsMockBinderConfiguration.class }))); - } - - private BindingServiceProperties createBindingServiceProperties( - HashMap properties) { - BindingServiceProperties bindingServiceProperties = new BindingServiceProperties(); - org.springframework.boot.context.properties.bind.Binder propertiesBinder; - propertiesBinder = new org.springframework.boot.context.properties.bind.Binder( - new MapConfigurationPropertySource(properties)); - propertiesBinder.bind("spring.cloud.stream", - org.springframework.boot.context.properties.bind.Bindable - .ofInstance(bindingServiceProperties)); - return bindingServiceProperties; - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class DefaultConsumerPropertiesTestSink { - - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class FooConfiguration { - - @Bean("input") - public Consumer> log() { - return System.out::println; - } - - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class BarConfiguration { - - @Bean - public Function myFunction() { - return s -> s; - } - - } - - public static class FooBinder - implements Binder { - - @Override - public Binding bindConsumer(String name, String group, - SomeBindableType inboundBindTarget, - ConsumerProperties consumerProperties) { - throw new UnsupportedOperationException(); - } - - @Override - public Binding bindProducer(String name, - SomeBindableType outboundBindTarget, - ProducerProperties producerProperties) { - throw new UnsupportedOperationException(); - } - - } - - public static class SomeBindableType { - - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/ExplicitBindingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/ExplicitBindingTests.java deleted file mode 100644 index 195ea6e34..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/ExplicitBindingTests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binding; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.MessageChannel; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * - */ -public class ExplicitBindingTests { - - @Test - void testExplicitBindings() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.input-bindings=fooin;barin", - "--spring.cloud.stream.output-bindings=fooout;barout")) { - - assertThat(context.getBean("fooin", MessageChannel.class)).isNotNull(); - assertThat(context.getBean("barin", MessageChannel.class)).isNotNull(); - assertThat(context.getBean("fooout", MessageChannel.class)).isNotNull(); - assertThat(context.getBean("barout", MessageChannel.class)).isNotNull(); - } - } - - @Test - void testExplicitBindingsWithExistingFunctionalBean() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ConsumerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.input-bindings=test")) { - - assertThat(context.getBean("test", MessageChannel.class)).isNotNull(); - } - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ConsumerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.input-bindings=test;test1")) { - - assertThat(context.getBean("test", MessageChannel.class)).isNotNull(); - assertThat(context.getBean("test1", MessageChannel.class)).isNotNull(); - } - } - - @Test - void testExplicitBindingsWithExistingConsumer() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ConsumerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.output-bindings=consume")) { - - assertThat(context.getBean("consume-in-0", MessageChannel.class)).isNotNull(); - assertThat(context.getBean("consume-out-0", MessageChannel.class)).isNotNull(); - } - } - - @Disabled - @Test - void testExplicitBindingsWithExistingSupplier() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SupplierConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.input-bindings=supply", - "--spring.cloud.stream.bindings.supply-out-0.producer.poller.fixed-delay=3000")) { - - - assertThat(context.getBean("supply-in-0", MessageChannel.class)).isNotNull(); - assertThat(context.getBean("supply-out-0", MessageChannel.class)).isNotNull(); - - OutputDestination output = context.getBean(OutputDestination.class); - assertThat(output.receive()).isNotNull(); - assertThat(output.receive(500)).isNull(); - assertThat(output.receive(500)).isNull(); - assertThat(output.receive(500)).isNull(); - assertThat(output.receive(500)).isNull(); - assertThat(output.receive(500)).isNull(); - assertThat(output.receive(500)).isNotNull(); - } - } - - @EnableAutoConfiguration - @Configuration - public static class EmptyConfiguration { - - } - - @EnableAutoConfiguration - @Configuration - public static class ConsumerConfiguration { - - @Bean - public Consumer consume() { - return System.out::println; - } - } - - @EnableAutoConfiguration - @Configuration - public static class SupplierConfiguration { - - @Bean - public Supplier supply() { - return () -> "hello"; - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/FluxMessageChannelBindingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/FluxMessageChannelBindingTests.java deleted file mode 100644 index fd0ff04b3..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/binding/FluxMessageChannelBindingTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binding; - -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.messaging.MessageChannel; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -public class FluxMessageChannelBindingTests { - - @Test - void testFluxMessageChannelBindingWhenReactiveOptIn() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.function.reactive.uppercase=true")) { - assertThat(context.getBean("uppercase-in-0", MessageChannel.class)).isInstanceOf(FluxMessageChannel.class); - assertThat(context.getBean("uppercase-out-0", MessageChannel.class)).isInstanceOf(FluxMessageChannel.class); - } - } - - @EnableAutoConfiguration - @Configuration - public static class ReactiveFunctionConfiguration { - - @Bean - public Function, Flux> uppercase() { - return s -> s.map(String::toUpperCase); - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/BindingHandlerAdviseTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/BindingHandlerAdviseTests.java deleted file mode 100644 index 575438a62..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/BindingHandlerAdviseTests.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.config; - -import java.util.function.Consumer; -import java.util.function.Function; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.validation.annotation.Validated; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -// see https://github.com/spring-cloud/spring-cloud-stream/issues/1573 for more details -/** - * @author Oleg Zhurakousky - * @author Soby Chacko - * - */ -public class BindingHandlerAdviseTests { - - @Test - @Disabled - void testFailureWithWrongValue() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder(SampleConfiguration.class).web(WebApplicationType.NONE).run("--props.value=-1", - "--spring.jmx.enabled=false")); - } - - @Test - void testValidatedValueValue() { - ValidatedProps validatedProps = new SpringApplicationBuilder(SampleConfiguration.class) - .web(WebApplicationType.NONE).run("--props.value=2", "--spring.jmx.enabled=false") - .getBean(ValidatedProps.class); - assertThat(validatedProps.getValue()).isEqualTo(2); - } - - @Test - void nonValidatedConfigProperties() { - new SpringApplicationBuilder(NonValidatedConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - // simply should not fail - } - - @Test - @Disabled - void validatedConfigProperties() { - assertThatExceptionOfType(ConfigurationPropertiesBindException.class) - .isThrownBy(() -> new SpringApplicationBuilder(ValidatedConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false")); - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class NonValidatedConfiguration { - - @Bean - public Function processor() { - return s -> s; - } - - @Bean - @ConfigurationProperties - public NonValidatedClass nonValidatedClass() { - return new NonValidatedClass(); - } - } - - public static class NonValidatedClass { - - private String id; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class ValidatedConfiguration { - - @Bean - public Function processor() { - return s -> s; - } - - @Bean - @ConfigurationProperties - public ValidatedClass nonValidatedClass() { - return new ValidatedClass(); - } - } - - @Validated - public static class ValidatedClass { - - private String id; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - } -} - -@Import(TestChannelBinderConfiguration.class) -@EnableAutoConfiguration -@EnableConfigurationProperties(ValidatedProps.class) -class SampleConfiguration { - - @Bean - public Consumer sink() { - return System.out::println; - } - -} - -@ConfigurationProperties("props") -@Validated -class ValidatedProps { - - private int value; - - public int getValue() { - return this.value; - } - - public void setValue(int value) { - this.value = value; - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/BindingServiceConfigurationTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/BindingServiceConfigurationTests.java deleted file mode 100644 index d9567aeed..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/BindingServiceConfigurationTests.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.config; - -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.integration.annotation.ServiceActivator; -import org.springframework.integration.channel.PublishSubscribeChannel; -import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Oleg Zhurakousky - * - */ -public class BindingServiceConfigurationTests { - - @Test - void testErroChannelDistributesMessagesInCaseOfException() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration - .getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run()) { - AtomicInteger counter = new AtomicInteger(); - PublishSubscribeChannel channel = context.getBean("errorChannel", PublishSubscribeChannel.class); - //channel.setIgnoreFailures(true); - channel.subscribe(m -> { - counter.incrementAndGet(); - throw new RuntimeException("one"); - }); - channel.subscribe(m -> { - counter.incrementAndGet(); - throw new RuntimeException("two"); - }); - channel.subscribe(m -> { - counter.incrementAndGet(); - throw new RuntimeException("three"); - }); - channel.send(new GenericMessage("foo")); - assertThat(counter.get()).isEqualTo(3); - } - } - - @Test - void valdateImportedConfiguartionHandlerPostProcessing() { - ApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration - .getCompleteConfiguration(RootConfiguration.class)) - .web(WebApplicationType.NONE).run(); - Map beansOfType = context - .getBeansOfType(AbstractReplyProducingMessageHandler.class); - for (AbstractReplyProducingMessageHandler handler : beansOfType.values()) { - assertThat(handler.getNotPropagatedHeaders().contains("contentType")) - .isTrue(); - } - } - - @Configuration - @Import(ImportedConfiguration.class) - public static class RootConfiguration { - - @ServiceActivator(inputChannel = "input") - public void rootService(String val) { - } - - } - - @Configuration - public static class ImportedConfiguration { - - @ServiceActivator(inputChannel = "input") - public void importedService(String val) { - } - - } - - @Configuration - public static class EmptyConfiguration { - - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/RetryTemplateTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/RetryTemplateTests.java deleted file mode 100644 index 41c0edc6b..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/config/RetryTemplateTests.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.config; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.annotation.StreamRetryTemplate; -import org.springframework.cloud.stream.binder.AbstractBinder; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.retry.support.RetryTemplate; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Oleg Zhurakousky - * @author Soby Chacko - */ -public class RetryTemplateTests { - - @SuppressWarnings("rawtypes") - @Test - void testSingleCustomRetryTemplate() throws Exception { - ApplicationContext context = new SpringApplicationBuilder( - SingleCustomRetryTemplateConfiguration.class).web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false"); - AbstractBinder binder = context.getBean(AbstractBinder.class); - Field f = AbstractBinder.class.getDeclaredField("consumerBindingRetryTemplates"); - f.setAccessible(true); - @SuppressWarnings("unchecked") - Map consumerBindingRetryTemplates = (Map) f - .get(binder); - assertThat(consumerBindingRetryTemplates).hasSize(1); - } - - @SuppressWarnings("rawtypes") - @Test - void testSpecificCustomRetryTemplate() throws Exception { - ApplicationContext context = new SpringApplicationBuilder( - SpecificCustomRetryTemplateConfiguration.class) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.processor-in-0.consumer.retry-template-name=retryTemplateTwo"); - - RetryTemplate retryTemplateTwo = context.getBean("retryTemplateTwo", - RetryTemplate.class); - BindingServiceProperties bindingServiceProperties = context - .getBean(BindingServiceProperties.class); - ConsumerProperties consumerProperties = bindingServiceProperties - .getConsumerProperties("processor-in-0"); - AbstractBinder binder = context.getBean(AbstractBinder.class); - - Method m = AbstractBinder.class.getDeclaredMethod("buildRetryTemplate", - ConsumerProperties.class); - m.setAccessible(true); - RetryTemplate retryTemplate = (RetryTemplate) m.invoke(binder, - consumerProperties); - assertThat(retryTemplate).isEqualTo(retryTemplateTwo); - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class SpecificCustomRetryTemplateConfiguration { - - @StreamRetryTemplate - public RetryTemplate retryTemplate() { - return new RetryTemplate(); - } - - @StreamRetryTemplate - public RetryTemplate retryTemplateTwo() { - return new RetryTemplate(); - } - - @Bean - public RetryTemplate otherRetryTemplate() { - return new RetryTemplate(); - } - - @Bean - public Function processor() { - return s -> s; - } - } - - @Import(TestChannelBinderConfiguration.class) - @EnableAutoConfiguration - public static class SingleCustomRetryTemplateConfiguration { - - @StreamRetryTemplate - public RetryTemplate retryTemplate() { - return new RetryTemplate(); - } - - @Bean - public RetryTemplate otherRetryTemplate() { - return new RetryTemplate(); - } - - @Bean - public Function processor() { - return s -> s; - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/endpoint/ActuatorBindingsTest.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/endpoint/ActuatorBindingsTest.java deleted file mode 100644 index 40b2f4f47..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/endpoint/ActuatorBindingsTest.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.endpoint; - -import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.binding.BindingsLifecycleController; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.util.ObjectUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * - * @author Original authors - * @author Soby Chacko - */ -public class ActuatorBindingsTest { - - /* - * Even though this test performs some simple assertions, the main purpose for it is to validate that - * it does not result in recursive exception described in https://github.com/spring-cloud/spring-cloud-stream/issues/2253 - */ - @Test - void test_2253() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(Bindings.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consume", - "--spring.jackson.visibility.field=ANY" // see https://github.com/spring-cloud/spring-cloud-stream/issues/2253 - // we need the above just to verify that such action does not - // interfere with instance of ObjectMapper inside of BindingsLifecycleController - )) { - - BindingsLifecycleController controller = context - .getBean(BindingsLifecycleController.class); - List> bindings = controller.queryStates(); - assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("bindingName")).isEqualTo("consume-in-0"); - } - } - - private static ClassLoader createClassLoader(String[] additionalClasspathDirectories) throws IOException { - URL[] urls = ObjectUtils.isEmpty(additionalClasspathDirectories) ? new URL[0] - : new URL[additionalClasspathDirectories.length]; - if (!ObjectUtils.isEmpty(additionalClasspathDirectories)) { - for (int i = 0; i < additionalClasspathDirectories.length; i++) { - urls[i] = new URL(new ClassPathResource(additionalClasspathDirectories[i]) - .getURL().toString() + "/"); - } - } - return new URLClassLoader(urls, - ActuatorBindingsTest.class.getClassLoader()); - } - - // Following three tests are verifying the behavior for - // https://github.com/spring-cloud/spring-cloud-stream/commit/3abf06345ad1ed57dea161b35503eba107feb04a - // More details are at: https://github.com/spring-cloud/spring-cloud-stream/issues/2716 - @Test - void whenTwoBindersFoundThrowErrorIfNoSpecificBinderIsChosen() throws Exception { - ClassLoader classLoader = createClassLoader(new String[] { "binder1" }); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(Bindings.class)) - .resourceLoader(new DefaultResourceLoader(classLoader)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consume" - )) { - - BindingsLifecycleController controller = context - .getBean(BindingsLifecycleController.class); - assertThatThrownBy(controller::queryStates).isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("More than one binder types found, but no binder specified on the binding"); - } - } - - @Test - void whenTwoBindersFoundNoErrorIfBinderProvidedThroughBinding() throws Exception { - ClassLoader classLoader = createClassLoader(new String[] { "binder1" }); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(Bindings.class)) - .resourceLoader(new DefaultResourceLoader(classLoader)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consume", - "--spring.cloud.stream.bindings.consume-in-0.binder=integration" - )) { - - BindingsLifecycleController controller = context - .getBean(BindingsLifecycleController.class); - List> bindings = controller.queryStates(); - assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("bindingName")).isEqualTo("consume-in-0"); - assertThat(bindings.get(0).get("binderName")).isEqualTo("integration"); - assertThat(bindings.get(0).get("binderType")).isEqualTo("integration"); - } - } - - @Test - void whenTwoBindersFoundNoErrorWhenDefaultBinderIsProvided() throws Exception { - ClassLoader classLoader = createClassLoader(new String[] { "binder1" }); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(Bindings.class)) - .resourceLoader(new DefaultResourceLoader(classLoader)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consume", - "--spring.cloud.stream.default-binder=integration1" - )) { - - BindingsLifecycleController controller = context - .getBean(BindingsLifecycleController.class); - List> bindings = controller.queryStates(); - assertThat(bindings.size()).isEqualTo(1); - assertThat(bindings.get(0).get("bindingName")).isEqualTo("consume-in-0"); - assertThat(bindings.get(0).get("binderName")).isEqualTo("integration1"); - assertThat(bindings.get(0).get("binderType")).isEqualTo("integration1"); - } - } - - @EnableAutoConfiguration - public static class Bindings { - - @Bean - public Consumer consume() { - return message -> System.out.println("Received message " + message); - } - - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/DynamicDestinationFunctionTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/DynamicDestinationFunctionTests.java deleted file mode 100644 index 2ba7ee8f4..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/DynamicDestinationFunctionTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Oleg Zhurakousky - * @author David Turanski - * - * - * TODO: Need to rewrite this test. - */ -public class DynamicDestinationFunctionTests { - - @AfterAll - public static void after() { - System.clearProperty("spring.cloud.stream.function.definition"); - System.clearProperty("spring.cloud.function.definition"); - } - - @Test - @Disabled - void testEmptyConfiguration() { - TestChannelBinderConfiguration.applicationContextRunner(SampleConfiguration.class) - .withPropertyValues( - "spring.jmx.enabled=false", - "spring.cloud.stream.bindings.fooDestination.producer.partitionKeyExtractorName=keyExtractor") - .run(context -> { - InputDestination input = context.getBean(InputDestination.class); - input.send(new GenericMessage("fooDestination")); - - BindingServiceProperties serviceProperties = context.getBean(BindingServiceProperties.class); - assertThat("keyExtractor").isEqualTo( - serviceProperties.getProducerProperties("fooDestination").getPartitionKeyExtractorName()); - - OutputDestination output = context.getBean(OutputDestination.class); - Object result = output.receive(1000).getPayload(); - assertThat(result).isEqualTo("fooDestination"); - }); - } - - @EnableAutoConfiguration - public static class SampleConfiguration { - -// @Autowired -// private BinderAwareChannelResolver resolver; - - @Bean - public PartitionKeyExtractorStrategy keyExtractor() { - return new PartitionKeyExtractorStrategy() { - - @Override - public Object extractKey(Message message) { - return 0; - } - }; - } - -// @Bean -// public Consumer cons() { -// return value -> { -// resolver.resolveDestination(value).send(new GenericMessage(value)); -// }; -// } - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/FunctionBatchingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/FunctionBatchingTests.java deleted file mode 100644 index 9a97473e1..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/FunctionBatchingTests.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; - -import org.assertj.core.util.Arrays; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.support.KafkaNull; -import org.springframework.messaging.Message; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Gary Russel - * @author Oleg Zhurakousky - * @author David Turanski - * - * @since 3.0 - */ -public class FunctionBatchingTests { - - @Test - void testMessageBatchConfigurationWithKafkaNull() { - TestChannelBinderConfiguration.applicationContextRunner(MessageBatchConfiguration.class) - .withPropertyValues("spring.jmx.enabled=false", - "spring.cloud.stream.function.definition=func", - "spring.cloud.stream.bindings.input.consumer.batch-mode=true") - .run(context -> { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - List list = new ArrayList<>(); - list.add("{\"name\":\"bob\"}".getBytes()); - list.add("{\"name\":\"jill\"}".getBytes()); - list.add(KafkaNull.INSTANCE); - list.add("{\"name\":\"steve\"}".getBytes()); - Message> inputMessage = MessageBuilder - .withPayload(list) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage).isNotNull(); - assertThat(outputMessage.getPayload()) - .isEqualTo("{\"name\":\"bob\"}".getBytes()); - - context.stop(); - }); - } - - @Test - void testListPayloadConfiguration() { - TestChannelBinderConfiguration.applicationContextRunner(ListPayloadNotBatchConfiguration.class) - .withPropertyValues("spring.jmx.enabled=false", - "spring.cloud.stream.function.definition=func") - .run(context -> { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes()) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage).isNotNull(); - assertThat(outputMessage.getPayload()) - .isEqualTo("{\"name\":\"bob\"}".getBytes()); - - context.stop(); - }); - } - - @Test - void testListStringPayloadConfigurationTextPlain() { - TestChannelBinderConfiguration.applicationContextRunner(ListStringPayloadConfiguration.class) - .withPropertyValues("spring.jmx.enabled=false", - "spring.cloud.stream.function.definition=func", - "spring.cloud.stream.bindings.func-in-0.content-type=text/plain") - .run(context -> { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - List bytes = Arrays.asList(new Object[] {"abc".getBytes(), "xyz".getBytes()}); - Message inputMessage = MessageBuilder.withPayload(bytes).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(new String(outputMessage.getPayload())).isEqualTo("[abc, xyz]"); - context.stop(); - }); - } - - @Test - void testListObjectPayloadObjectConfigurationTextPlain() { - TestChannelBinderConfiguration.applicationContextRunner(ListObjectPayloadConfiguration.class) - .withPropertyValues("spring.jmx.enabled=false", - "spring.cloud.stream.function.definition=func", - "spring.cloud.stream.bindings.func-in-0.content-type=text/plain") - .run(context -> { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - List bytes = Arrays.asList(new Object[] {"abc".getBytes(), "xyz".getBytes()}); - Message inputMessage = MessageBuilder.withPayload(bytes).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(new String(outputMessage.getPayload())).isEqualTo("[abc, xyz]"); - context.stop(); - }); - } - - @Test - void testSimpleBatchConfiguration() { - TestChannelBinderConfiguration.applicationContextRunner(SimpleBatchConfiguration.class) - .withPropertyValues( - "spring.jmx.enabled=false", - "spring.cloud.stream.function.definition=func", - "spring.cloud.stream.bindings.input.consumer.batch-mode=true") - .run(context -> { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - List list = new ArrayList<>(); - list.add("{\"name\":\"bob\"}".getBytes()); - list.add("{\"name\":\"jill\"}".getBytes()); - Message> inputMessage = MessageBuilder - .withPayload(list) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage).isNotNull(); - assertThat(outputMessage.getPayload()) - .isEqualTo("{\"name\":\"bob\"}".getBytes()); - context.stop(); - }); - } - - @Test - void testNestedBatchConfiguration() { - TestChannelBinderConfiguration.applicationContextRunner(NestedBatchConfiguration.class) - .withPropertyValues("spring.jmx.enabled=false", - "spring.cloud.stream.function.definition=func", - "spring.cloud.stream.bindings.input.consumer.batch-mode=true") - .run(context -> { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - List list = new ArrayList<>(); - list.add("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes()); - Message> inputMessage = MessageBuilder - .withPayload(list) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage).isNotNull(); - assertThat(outputMessage.getPayload()) - .isEqualTo("{\"name\":\"bob\"}".getBytes()); - context.stop(); - }); - } - - @EnableAutoConfiguration - public static class SimpleBatchConfiguration { - - @Bean - public Function, Person> func() { - return x -> x.get(0); - } - - public static class Person { - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - } - - } - - @EnableAutoConfiguration - public static class ListStringPayloadConfiguration { - @Bean - public Function, String> func() { - return x -> x.toString(); - } - } - - @EnableAutoConfiguration - public static class ListObjectPayloadConfiguration { - @Bean - public Function, String> func() { - return x -> x.toString(); - } - } - - @EnableAutoConfiguration - public static class ListPayloadNotBatchConfiguration { - - @Bean - public Function, Person> func() { - return x -> x.get(0); - } - - public static class Person { - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - } - - @EnableAutoConfiguration - public static class NestedBatchConfiguration { - - @Bean - public Function>, Person> func() { - return x -> x.get(0).get(0); - } - - public static class Person { - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - } - - } - - @EnableAutoConfiguration - public static class MessageBatchConfiguration { - - @Bean - public Function>, Person> func() { - return x -> { - Object o = x.getPayload().get(2); - assertThat(o).isNull(); - return (Person) x.getPayload().get(0); - }; - } - - public static class Person { - - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - } - - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/FunctionPostProcessingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/FunctionPostProcessingTests.java deleted file mode 100644 index 9ba017000..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/FunctionPostProcessingTests.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.util.function.Function; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.function.context.PostProcessingFunction; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Oleg Zhurakousky - * - */ -public class FunctionPostProcessingTests { - - @Test - void testNothingIsBroken() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=echo")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("hello".getBytes()); - } - } - - @Test - void testSuccessfulPostProcessingOfSingleFunction() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("HELLO".getBytes()); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isTrue(); - } - } - - @Test - void testNoPostProcessingOnError() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("error".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive()).isNull(); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isFalse(); - } - } - - @Test - void testNoFailureOnPostProcessingError() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("post_processing_error".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("POST_PROCESSING_ERROR".getBytes()); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isFalse(); - } - } - - - @Test - void testWithCompositionLastFunctionIsPostProcessing() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=echo|uppercase")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("HELLO".getBytes()); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isTrue(); - } - } - - @Test - void testWithCompositionFirstFunctionIsPostProcessing() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase|echo")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("HELLO".getBytes()); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isFalse(); - } - } - - @Test - void testOlnyLastPostProcessorInvoked() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=echo|uppercase|reverse")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("OLLEH".getBytes()); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isFalse(); - assertThat(context.getBean(SingleFunctionPostProcessingFunction2.class).success).isTrue(); - } - } - - @Test - void testOlnyLastPostProcessorInvoked2() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(PostProcessingTestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase|echo|reverse")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - inputDestination.send(inputMessage); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive().getPayload()).isEqualTo("OLLEH".getBytes()); - assertThat(context.getBean(SingleFunctionPostProcessingFunction.class).success).isFalse(); - assertThat(context.getBean(SingleFunctionPostProcessingFunction2.class).success).isTrue(); - } - } - - - @EnableAutoConfiguration - public static class PostProcessingTestConfiguration { - - @Bean - public Function echo() { - return x -> x; - } - - @Bean - public Function uppercase() { - return new SingleFunctionPostProcessingFunction(); - } - - @Bean - public Function reverse() { - return new SingleFunctionPostProcessingFunction2(); - } - } - - private static class SingleFunctionPostProcessingFunction implements PostProcessingFunction { - - private boolean success; - - @Override - public String apply(String input) { - if (input.equals("error")) { - throw new RuntimeException("intentional"); - } - return input.toUpperCase(); - } - - @Override - public void postProcess(Message result) { - if (result.getPayload().equals("POST_PROCESSING_ERROR")) { - throw new RuntimeException("intentional"); - } - success = true; - } - } - - private static class SingleFunctionPostProcessingFunction2 implements PostProcessingFunction { - - private boolean success; - - @Override - public String apply(String input) { - return new StringBuilder(input).reverse().toString(); - } - - @Override - public void postProcess(Message result) { - success = true; - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java deleted file mode 100644 index f6dc7ac62..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/GreenfieldFunctionEnableBindingTests.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.function; - -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.messaging.PollableChannel; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This test validates proper function binding for applications where EnableBinding is - * declared. - * - * @author Oleg Zhurakousky - * @author Artem Bilan - */ -public class GreenfieldFunctionEnableBindingTests { - - @Test - void testProcessorFromFunction() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ProcessorFromFunction.class)).web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=toUpperCase", - "--spring.jmx.enabled=false")) { - - InputDestination source = context.getBean(InputDestination.class); - source.send(new GenericMessage("John Doe".getBytes())); - OutputDestination target = context.getBean(OutputDestination.class); - assertThat(target.receive(10000).getPayload()) - .isEqualTo("JOHN DOE".getBytes(StandardCharsets.UTF_8)); - } - } - - @Test - void testSinkFromConsumer() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration - .getCompleteConfiguration(SinkFromConsumer.class)) - .web(WebApplicationType.NONE) - .run("--spring.cloud.function.definition=sink", - "--spring.jmx.enabled=false")) { - - InputDestination source = context.getBean(InputDestination.class); - PollableChannel result = context.getBean("result", PollableChannel.class); - source.send(new GenericMessage("John Doe".getBytes())); - assertThat(result.receive(10000).getPayload()).isEqualTo("John Doe"); - } - } - - @EnableAutoConfiguration - public static class SourceFromSupplier { - - @Bean - public Supplier date() { - return () -> new Date(12345L); - } - - } - - @EnableAutoConfiguration - public static class ProcessorFromFunction { - - @Bean - public Function toUpperCase() { - return String::toUpperCase; - } - - } - - @EnableAutoConfiguration - public static class SinkFromConsumer { - - @Bean - public PollableChannel result() { - return new QueueChannel(); - } - - @Bean - public Consumer sink(PollableChannel result) { - return s -> { - result.send(new GenericMessage(s)); - System.out.println(s); - }; - } - - } - - static class Foo { - - String bar; - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/ImplicitFunctionBindingTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/ImplicitFunctionBindingTests.java deleted file mode 100644 index 77586b56b..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/ImplicitFunctionBindingTests.java +++ /dev/null @@ -1,1720 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.core.publisher.Sinks.Many; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.catalog.FunctionAroundWrapper; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.BindingCreatedEvent; -import org.springframework.cloud.stream.binder.test.FunctionBindingTestUtils; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.binding.BindingsLifecycleController; -import org.springframework.cloud.stream.binding.BindingsLifecycleController.State; -import org.springframework.cloud.stream.binding.DefaultPartitioningInterceptor; -import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.core.ResolvableType; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.dsl.IntegrationFlow; -import org.springframework.integration.scheduling.PollerMetadata; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.kafka.support.KafkaNull; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.scheduling.support.PeriodicTrigger; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; - -/** - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * - */ -public class ImplicitFunctionBindingTests { - - @AfterEach - public void after() { - System.clearProperty("spring.cloud.function.definition"); - } - - - @Test - void testFailedApplicationListenerConfiguration() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(FailedApplicationListenerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=echo")) { - - InputDestination input = context.getBean(InputDestination.class); - input.send(new GenericMessage("hello".getBytes()), "echo-in-0"); - - OutputDestination output = context.getBean(OutputDestination.class); - Message result = output.receive(1000, "echo-out-0"); - assertThat(result.getPayload()).isEqualTo("hello".getBytes()); - } - } - - @SuppressWarnings({"rawtypes"}) - @Test - void testDisableAutodetect() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SendToDestinationConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.stream.function.autodetect=false")) { - - BindingsLifecycleController ctrl = context.getBean(BindingsLifecycleController.class); - var input = ctrl.queryState("echo-in-0"); - var output = ctrl.queryState("echo-out-0"); - assertThat(input).isEmpty(); - assertThat(output).isEmpty(); - } - } - - - @SuppressWarnings({"rawtypes" }) - @Test - void testBindingControl() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SendToDestinationConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false")) { - - BindingsLifecycleController ctrl = context.getBean(BindingsLifecycleController.class); - Binding input = ctrl.queryState("echo-in-0").get(0); - assertThat(input.isRunning()).isTrue(); - ctrl.changeState("echo-in-0", State.STOPPED); - assertThat(input.isRunning()).isFalse(); - } - } - - @ParameterizedTest - @ValueSource(strings = {"single-destination", "destination1,destination2,destination3"}) - void testGh2658_WithMultipleDestinations(String destination) { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleConsumerWithMultipleDestinationConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consumerMultiple", - "--spring.cloud.stream.bindings.consumerMultiple-in-0.group=group", - "--spring.cloud.stream.bindings.consumerMultiple-in-0.destination=" + destination)) { - - BindingsLifecycleController ctrl = context.getBean(BindingsLifecycleController.class); - var multipleInput = ctrl.queryState("consumerMultiple-in-0"); - - assertThat(multipleInput).hasSize(destination.split(",").length); - multipleInput.stream().forEach(binding -> assertThat(binding.isRunning()).isTrue()); - - ctrl.changeState("consumerMultiple-in-0", State.STOPPED); - multipleInput.stream().forEach(binding -> assertThat(binding.isRunning()).isFalse()); - - ctrl.changeState("consumerMultiple-in-0", State.STARTED); - multipleInput.stream().forEach(binding -> assertThat(binding.isRunning()).isTrue()); - } - } - - @Test - void testGh2658_queryBindingThatNotExists() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleConsumerWithMultipleDestinationConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=consumerMultiple", - "--spring.cloud.stream.bindings.consumerMultiple-in-0.group=group", - "--spring.cloud.stream.bindings.consumerMultiple-in-0.destination=destination")) { - - BindingsLifecycleController ctrl = context.getBean(BindingsLifecycleController.class); - var inputBindingList = ctrl.queryState("bindingNotExist-in-0"); - assertThat(inputBindingList).isEmpty(); - } - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - @Test - void dynamicBindingTestWithFunctionRegistrationAndExplicitDestination() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.stream.bindings.function-in-0.destination=input")) { - - InputDestination input = context.getBean(InputDestination.class); - try { - input.send(new GenericMessage("hello".getBytes())); - fail(); // it should since there are no functions and no bindings - } - catch (Exception e) { - // good, we expected it - } - - Function function = v -> v; - FunctionRegistration functionRegistration = new FunctionRegistration(function, "function"); - functionRegistration = functionRegistration.type(ResolvableType.forClassWithGenerics(Function.class, Object.class, Object.class).getType()); - FunctionBindingTestUtils.bind(context, functionRegistration); - - input.send(new GenericMessage("hello".getBytes()), "input"); - - OutputDestination output = context.getBean(OutputDestination.class); - assertThat(output.receive(1000, "function-out-0").getPayload()).isEqualTo("hello".getBytes()); - } - } - - @Test - void dynamicBindingTestWithFunction() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false")) { - InputDestination input = context.getBean(InputDestination.class); - try { - input.send(new GenericMessage("hello".getBytes())); - fail(); // it should since there are no functions and no bindings - } - catch (Exception e) { - // good, we expected it - } - - Function function = v -> new String(v).toUpperCase(); - FunctionBindingTestUtils.bind(context, function); - - input.send(new GenericMessage("hello".getBytes())); - - OutputDestination output = context.getBean(OutputDestination.class); - assertThat(new String(output.receive().getPayload())).isEqualTo("HELLO"); - } - } - - @Test - void testEmptyConfiguration() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--debug")) { - context.getBean(InputDestination.class); - } - catch (Exception e) { // should not fail - fail(); - } - } - - @Test - void testNullMessage() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NullMessagerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=func; funcMessage")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - - Message inputMessage = MessageBuilder.withPayload(KafkaNull.INSTANCE).build(); - inputDestination.send(inputMessage, "func-in-0"); - - Message outputMessage = outputDestination.receive(1000, "func-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("NULL".getBytes()); - - inputDestination.send(inputMessage, "funcMessage-in-0"); - - outputMessage = outputDestination.receive(1000, "funcMessage-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("NULL".getBytes()); - } - } - - @Test - void testSimpleFunctionWithStreamProperty() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoEnableBindingConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=func")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - - } - } - - @Test - void testHeaderRetentionWithComposition() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoEnableBindingConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.function.autodetect=false", - "--spring.cloud.function.definition=func|addHeaders")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - - } - } - - @Test - void testReactiveFunctionWithState() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoEnableBindingConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=aggregate")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - inputDestination.send(inputMessage); - inputDestination.send(inputMessage); - assertThat(new String(outputDestination.receive(2000, "aggregate-out-0").getPayload())).isEqualTo("HelloHelloHello"); - assertThat(new String(outputDestination.receive(2000, "aggregate-out-0").getPayload())).isEqualTo(""); - - inputDestination.send(inputMessage); - inputDestination.send(inputMessage); - inputDestination.send(inputMessage); - inputDestination.send(inputMessage); - assertThat(new String(outputDestination.receive(2000, "aggregate-out-0").getPayload())).isEqualTo("HelloHelloHelloHello"); - } - } - - @SuppressWarnings("rawtypes") - @Test - void testFunctionWithUseNativeEncoding() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoEnableBindingConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=func", - "--spring.cloud.stream.bindings.func-out-0.producer.useNativeEncoding=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello"); - } - } - - @Test - void testSimpleFunctionWithNativeProperty() { - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoEnableBindingConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=func")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - - } - } - - @Test - void testSimpleFunctionWithoutDefinitionProperty() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - - } - } - - @Test - void testSimpleConsumerWithoutDefinitionProperty() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - assertThat(System.getProperty("consumer")).isEqualTo("Hello"); - System.clearProperty("consumer"); - } - } - - @Test - void fooFunctionComposedWithConsumerNoOutputChannel() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class, SingleReactiveConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=echo|consumer")) { - - assertThat(context.containsBean("echoconsumer-out-0")).isFalse(); - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - assertThat(System.getProperty("consumer")).isEqualTo("Hello"); - System.clearProperty("consumer"); - } - } - - @Test - void fooFunctionComposedWithConsumerNonReactive() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(FunctionConsumerCopositionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=echo|consumer")) { - - assertThat(context.containsBean("echoconsumer-out-0")).isFalse(); - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - assertThat(System.getProperty("FunctionConsumerCopositionConfiguration")).isEqualTo("Hello"); - System.clearProperty("FunctionConsumerCopositionConfiguration"); - } - } - - @Test - void testReactiveConsumerWithoutDefinitionProperty() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleReactiveConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - assertThat(System.getProperty("consumer")).isEqualTo("Hello"); - System.clearProperty("consumer"); - } - } - - @Test - void testConsumer() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=consumer", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.consumer-in-0.content-type=text/plain")) { - - InputDestination source = context.getBean(InputDestination.class); - source.send(new GenericMessage("John Doe".getBytes())); - } - } - - @Test - void testBindingWithReactiveFunction() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=echo")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessageOne = MessageBuilder.withPayload("Hello".getBytes()).build(); - Message inputMessageTwo = MessageBuilder.withPayload("Hello Again".getBytes()).build(); - inputDestination.send(inputMessageOne); - inputDestination.send(inputMessageTwo); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello Again".getBytes()); - } - } - -// @Test -// public void testFunctionConfigDisabledIfStreamListenerIsUsed() { -// System.clearProperty("spring.cloud.function.definition"); -// try (ConfigurableApplicationContext context = new SpringApplicationBuilder( -// TestChannelBinderConfiguration.getCompleteConfiguration(LegacyConfiguration.class)) -// .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { -// -// assertThat(context.getBean("supplierInitializer").getClass().getSimpleName()).isEqualTo("NullBean"); -// } -// } - - @Test - void testDeclaredTypeVsActualInstance() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SCF_GH_409Configuration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - - Message inputMessageOne = MessageBuilder.withPayload("Hello".getBytes()).build(); - - inputDestination.send(inputMessageOne); - fail(); - } - catch (Exception ex) { - // good - } - } - - @Test - void testWithContextTypeApplicationProperty() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.input.content-type=text/plain", "--debug")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessageOne = MessageBuilder.withPayload("Hello".getBytes()).build(); - Message inputMessageTwo = MessageBuilder.withPayload("Hello Again".getBytes()).build(); - inputDestination.send(inputMessageOne); - inputDestination.send(inputMessageTwo); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello Again".getBytes()); - } - } - - @Test - void testWithIntegrationFlowAsFunction() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(FunctionSampleSpringIntegrationConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("hello".getBytes()).build(); - - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes()); - } - } - - @Test - void testSupplierWithCustomPoller() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SupplierWithExplicitPollerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.stream.poller.fixed-delay=1500")) { - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class); - assertThat(((PeriodicTrigger) pollerMetadata.getTrigger()).getPeriodDuration()).isEqualTo(Duration.ofMillis(1500)); - - Message outputMessage = outputDestination.receive(6000); - assertThat(outputMessage.getPayload()).isEqualTo("hello".getBytes()); - } - } - - @Test - @Disabled - void testImperativeSupplierReactiveFunctionComposition() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ImperativeSupplierComposedWithReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.poller.fixed-delay=1000", - "--spring.cloud.function.definition=supplier|functionA")) { - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message outputMessage = outputDestination.receive(2000, "supplierfunctionA-out-0"); - Long value = Long.parseLong(new String(outputMessage.getPayload())); - - outputMessage = outputDestination.receive(5000, "supplierfunctionA-out-0"); - assertThat(Long.parseLong(new String(outputMessage.getPayload())) - value).isGreaterThanOrEqualTo(1000); - - outputMessage = outputDestination.receive(5000, "supplierfunctionA-out-0"); - assertThat(Long.parseLong(new String(outputMessage.getPayload())) - value).isGreaterThanOrEqualTo(1000); - - outputMessage = outputDestination.receive(5000, "supplierfunctionA-out-0"); - assertThat(Long.parseLong(new String(outputMessage.getPayload())) - value).isGreaterThanOrEqualTo(1000); - } - } - - - - @Test - void testSupplierWithCustomPollerAndMappedOutput() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SupplierWithExplicitPollerConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.stream.poller.fixed-delay=1200", - "--spring.cloud.function.bindings.supplier-out-0=output")) { - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class); - assertThat(((PeriodicTrigger) pollerMetadata.getTrigger()).getPeriodDuration()).isEqualTo(Duration.ofMillis(1200)); - - Message outputMessage = outputDestination.receive(10000); - assertThat(outputMessage.getPayload()).isEqualTo("hello".getBytes()); - } - } - - @Test - void testNoFunctionEnabledConfiguration() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(NoFunctionEnabledConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - try { - context.getBean(FunctionConfiguration.class); - fail(); - } - catch (Exception e) { - // ignore - } - - } - } - - @Test - void functionInvocationWrapperReflectsBiConsumerTargetFunctionType() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(WrappedBiConsumerAutoConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class); - FunctionInvocationWrapper functionWrapper = functionCatalog.lookup("testBiConsumer"); - assertThat(functionWrapper.isWrappedBiConsumer()).isTrue(); - } - } - - @Test - void testCollectionAndMapConversionDuringComposition() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(CompositionWithCollectionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=funcA|funcB", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("[{\"key1\":1, \"key2\":2},{\"key3\":3}]".getBytes()).build(); - - inputDestination.send(inputMessage); - String result = new String(outputDestination.receive().getPayload()); - - assertThat(result).isEqualTo("[{\"key1\":\"1\",\"key2\":\"2\"},{\"key3\":\"3\"}]"); - } - } - - // see https://github.com/spring-cloud/spring-cloud-stream/issues/1896 - @Test - void testOutputAsCollectionOfMessages() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(SplittableTypesConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=funcCollectionOfMessages", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("aa,bb,cc,dd".getBytes()).build(); - - inputDestination.send(inputMessage); - - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("aa"); - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("bb"); - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("cc"); - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("dd"); - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void testOutputAsArrayOfMessages() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(SplittableTypesConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=funcArrayOfMessages", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("aa,bb,cc,dd".getBytes()).build(); - - inputDestination.send(inputMessage); - - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("aa"); - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("bb"); - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("cc"); - assertThat(new String(outputDestination.receive(100).getPayload())).isEqualTo("dd"); - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void testWithExplicitBindingInstructions() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(SplittableTypesConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=funcArrayOfMessages", - "--spring.cloud.stream.function.bindings.funcArrayOfMessages-in-0=input", - "--spring.cloud.stream.function.bindings.funcArrayOfMessages-out-0=output", - "--spring.cloud.stream.bindings.input.destination=myInput", - "--spring.cloud.stream.bindings.output.destination=myOutput", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("aa,bb,cc,dd".getBytes()).build(); - - inputDestination.send(inputMessage, "myInput"); - - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("aa"); - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("bb"); - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("cc"); - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("dd"); - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void testWithExplicitBindingInstructionsOnlyDestination() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(SplittableTypesConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=funcArrayOfMessages", - "--spring.cloud.stream.bindings.funcArrayOfMessages-in-0.destination=myInput", - "--spring.cloud.stream.bindings.funcArrayOfMessages-out-0.destination=myOutput", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("aa,bb,cc,dd".getBytes()).build(); - - inputDestination.send(inputMessage, "myInput"); - - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("aa"); - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("bb"); - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("cc"); - assertThat(new String(outputDestination.receive(100, "myOutput").getPayload())).isEqualTo("dd"); - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void partitionOnOutputPayloadTest() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PojoFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=func", - "--spring.cloud.stream.bindings.func-out-0.producer.partitionKeyExpression=payload.id", - "--spring.cloud.stream.bindings.func-out-0.producer.partitionCount=5", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Jim Lahey".getBytes()).build(); - - inputDestination.send(inputMessage, "func-in-0"); - - assertThat(outputDestination.receive(100, "func-out-0").getHeaders().get("scst_partition")).isEqualTo(3); - - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void partitionOnOutputPayloadAsListAndFunctionAroundWrapperTest() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PojoFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=uppercase|persons", - "--spring.cloud.stream.bindings.uppercase|persons-out-0.producer.partitionKeyExpression=payload.id", - "--spring.cloud.stream.bindings.uppercase|persons-out-0.producer.partitionCount=5", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Jim Lahey".getBytes()).build(); - - inputDestination.send(inputMessage, "uppercasepersons-in-0"); - - assertThat(outputDestination.receive(100, "uppercasepersons-out-0").getHeaders().get("scst_partition")).isEqualTo(3); - - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void partitionOnOutputPayloadTestReactive() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PojoFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=funcReactive", - "--spring.cloud.stream.bindings.funcReactive-out-0.producer.partitionKeyExpression=payload.id", - "--spring.cloud.stream.bindings.funcReactive-out-0.producer.partitionCount=5", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Jim Lahey".getBytes()).build(); - - inputDestination.send(inputMessage, "funcReactive-in-0"); - - assertThat(outputDestination.receive(100, "funcReactive-out-0").getHeaders().get("scst_partition")).isEqualTo(3); - - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void partitionOnOutputPayloadWithSupplierTest() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PojoFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=personSupplier", - "--spring.cloud.stream.bindings.personSupplier-out-0.producer.partitionKeyExpression=payload.id", - "--spring.cloud.stream.bindings.personSupplier-out-0.producer.partitionCount=5", - "--spring.jmx.enabled=false")) { - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive(1000, "personSupplier-out-0").getHeaders().get("scst_partition")).isEqualTo(1); - - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void partitionOnOutputPayloadWithReactiveSupplierTest() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PojoFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=reactivePersonSupplier", - "--spring.cloud.stream.bindings.reactivePersonSupplier-out-0.producer.partitionKeyExpression=payload.id", - "--spring.cloud.stream.bindings.reactivePersonSupplier-out-0.producer.partitionCount=5", - "--spring.jmx.enabled=false")) { - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - assertThat(outputDestination.receive(1000, "reactivePersonSupplier-out-0").getHeaders().get("scst_partition")).isEqualTo(1); - - assertThat(outputDestination.receive(100)).isNull(); - } - } - - @Test - void contentTypeAsByteArrayTest() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PojoFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.function.definition=echoPerson", - "--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("{\"name\":\"Jim Lahey\",\"id\":420}".getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, "application/json".getBytes(StandardCharsets.UTF_8)) - .build(); - - inputDestination.send(inputMessage, "echoPerson-in-0"); - - assertThat(outputDestination.receive(100, "echoPerson-out-0").getPayload()).isEqualTo("{\"name\":\"Jim Lahey\",\"id\":420}".getBytes()); - } - } - - @Test - void testReactiveFunctionWithOutputAsMonoVoid() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(FunctionalConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - assertThat(context.containsBean("funcConsumer-out-0")).isFalse(); - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - assertThat(System.getProperty("consumer")).isEqualTo("Hello"); - System.clearProperty("consumer"); - } - } - - @Test - void testReactiveSendToDestinationConfiguration() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SendToDestinationConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message inputMessage = MessageBuilder.withPayload("aa".getBytes()).build(); - inputDestination.send(inputMessage, "echo-in-0"); - Message receivedMessage = outputDestination.receive(1000, "aa"); - - assertThat(receivedMessage.getPayload()).isEqualTo("aa".getBytes()); - assertThat(receivedMessage.getHeaders().get("spring.cloud.stream.sendto.destination")).isNotNull(); - - inputMessage = MessageBuilder.withPayload("bb".getBytes()).build(); - inputDestination.send(inputMessage, "echo-in-0"); - receivedMessage = outputDestination.receive(1000, "bb"); - - assertThat(receivedMessage.getPayload()).isEqualTo("bb".getBytes()); - assertThat(receivedMessage.getHeaders().get("spring.cloud.stream.sendto.destination")).isNotNull(); - } - } - - // See this issue for more context on this test: https://github.com/spring-cloud/spring-cloud-stream/issues/2770 - @Test - void testSendToDestinationWhenPartitionsEnabled() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ImperativeSendToDestinationConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - // partition-key-extractor also works below, but for easy testing we picked partition-key-expression since - // partition-key-extractor requires defining a bean. - "--spring.cloud.stream.bindings.aa.producer.partition-key-expression=payload", - "--spring.cloud.stream.bindings.bb.producer.partition-key-expression=payload")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message inputMessage = MessageBuilder.withPayload("aa".getBytes()).build(); - inputDestination.send(inputMessage, "echo-in-0"); - Message receivedMessage = outputDestination.receive(1000, "aa"); - - StreamBridge streamBridge = context.getBean(StreamBridge.class); - MessageChannel aa = streamBridge.resolveDestination("aa", null, null); - List interceptors1 = ((AbstractMessageChannel) aa).getInterceptors(); - - assertThat(interceptors1.size()).isEqualTo(1); - assertThat(interceptors1.get(0)).isInstanceOf(DefaultPartitioningInterceptor.class); - - assertThat(receivedMessage.getPayload()).isEqualTo("aa".getBytes()); - assertThat(receivedMessage.getHeaders().get("spring.cloud.stream.sendto.destination")).isNotNull(); - - inputMessage = MessageBuilder.withPayload("bb".getBytes()).build(); - inputDestination.send(inputMessage, "echo-in-0"); - receivedMessage = outputDestination.receive(1000, "bb"); - - MessageChannel bb = streamBridge.resolveDestination("bb", null, null); - List interceptors2 = ((AbstractMessageChannel) bb).getInterceptors(); - - assertThat(interceptors2.size()).isEqualTo(1); - assertThat(interceptors2.get(0)).isInstanceOf(DefaultPartitioningInterceptor.class); - - assertThat(receivedMessage.getPayload()).isEqualTo("bb".getBytes()); - assertThat(receivedMessage.getHeaders().get("spring.cloud.stream.sendto.destination")).isNotNull(); - } - } - - - @Test - @ExtendWith(OutputCaptureExtension.class) - void testReactiveConsumerWithConcurrencyGreaterThanOneLogsWarning(CapturedOutput output) { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext ignored = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveConsumerWithConcurrencyFailureConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.input-in-0.consumer.concurrency=2")) { - assertThat(output).contains("When using concurrency > 1 in reactive contexts, please make sure that you are using a reactive binder"); - } - } - - @Test - void testGh1973() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SupplierAndProcessorConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=echo;supplier", - "--spring.cloud.stream.bindings.supplier-out-0.destination=output", - "--spring.cloud.stream.bindings.echo-out-0.destination=output")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - inputDestination.send(MessageBuilder.withPayload("hello").build()); - - assertThat(outputDestination.receive(1000, "output")).isNotNull(); - assertThat(outputDestination.receive(1000, "output")).isNull(); - assertThat(outputDestination.receive(1000, "output")).isNull(); - - } - } - - @Test - void testGh1944() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SupplierAndPojoConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=supplier")) { - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message result = outputDestination.receive(1000, "supplier-out-0"); - assertThat(new String(result.getPayload())).isEqualTo("[{\"name\":\"Ricky\",\"id\":1},{\"name\":\"Julien\",\"id\":2}]"); - } - } - - @Test - void testGh2054() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=reactivePojoMessage")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - String jsonPerson = "{\"name\":\"Ricky\",\"id\":1}"; - - inputDestination.send(MessageBuilder.withPayload(jsonPerson.getBytes()).build()); - - Message result = outputDestination.receive(2000); - assertThat(new String(result.getPayload())).isEqualTo("{\"name\":\"RICKY\",\"id\":1}"); - } - } - - - @Test - void testGh2062() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=echo")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - String jsonPerson = "error"; - - inputDestination.send(MessageBuilder.withPayload(jsonPerson.getBytes()).build()); - // there is really nothing to assert other then check the logs for stack trace and error message - } - } - - @SuppressWarnings("rawtypes") - @Test - void testWithNativeEncodingImperative() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration2.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=imperative")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - inputDestination.send(new GenericMessage("hello".getBytes())); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message result = outputDestination.receive(2000); - assertThat(result.getPayload()).isInstanceOf(byte[].class); // check output type - assertThat(new String((byte[]) result.getPayload())).isEqualTo("byte[]"); // check input type - } - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration2.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=imperative", - "--spring.cloud.stream.bindings.imperative-in-0.consumer.useNativeDecoding=true", - "--spring.cloud.stream.bindings.imperative-out-0.producer.useNativeEncoding=true" - )) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - inputDestination.send(new GenericMessage("hello".getBytes())); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message result = outputDestination.receive(2000); - assertThat(result.getPayload()).isInstanceOf(String.class); // check output type - assertThat(result.getPayload()).isEqualTo("byte[]"); // check input type - } - } - - @SuppressWarnings("rawtypes") - @Test - void testWithNativeEncodingReactive() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration2.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=reactive")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - inputDestination.send(new GenericMessage("hello".getBytes())); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message result = outputDestination.receive(2000); - assertThat(result.getPayload()).isInstanceOf(byte[].class); // check output type - assertThat(new String((byte[]) result.getPayload())).isEqualTo("byte[]"); // check input type - } - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration2.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=reactive", - "--spring.cloud.stream.bindings.reactive-in-0.consumer.useNativeDecoding=true", - "--spring.cloud.stream.bindings.reactive-out-0.producer.useNativeEncoding=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - inputDestination.send(new GenericMessage("hello".getBytes())); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message result = outputDestination.receive(2000); - assertThat(result.getPayload()).isInstanceOf(String.class); // no output conversion to byte[] has happened. - assertThat(result.getPayload()).isEqualTo("byte[]"); - } - - //Consumer reactiveConsumer - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration2.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=reactiveConsumer", - "--spring.cloud.stream.bindings.reactiveConsumer-in-0.consumer.useNativeDecoding=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - inputDestination.send(new GenericMessage("hello".getBytes())); - - QueueChannel testChannel = context.getBean("testChannel", QueueChannel.class); - - Message result = testChannel.receive(2000); - assertThat(result.getPayload()).isEqualTo(byte[].class.getName()); - } - - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration2.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=reactiveFunctionConsumer", - "--spring.cloud.stream.bindings.reactiveFunctionConsumer-in-0.consumer.useNativeDecoding=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - inputDestination.send(new GenericMessage("hello".getBytes())); - - QueueChannel testChannel = context.getBean("testChannel", QueueChannel.class); - - Message result = testChannel.receive(2000); - assertThat(result.getPayload()).isEqualTo(byte[].class.getName()); - } - } - - @Test - void testHeaderPropagation() { - System.clearProperty("spring.cloud.function.definition"); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SingleFunctionConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=func")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - inputDestination.send(MessageBuilder.withPayload("hello").setHeader("foo", "bar").build()); - - assertThat(outputDestination.receive(1000).getHeaders().get("foo")).isEqualTo("bar"); - } - } - - - @EnableAutoConfiguration - public static class SupplierAndPojoConfiguration { - @Bean - public Supplier> supplier() { - Person p1 = new Person(); - p1.setId(1); - p1.setName("Ricky"); - Person p2 = new Person(); - p2.setId(2); - p2.setName("Julien"); - return () -> Arrays.asList(p1, p2); - } - } - - @EnableAutoConfiguration - public static class SupplierAndProcessorConfiguration { - Many> sink = Sinks.many().unicast().onBackpressureBuffer(); - - @Bean - public Supplier>> supplier() { - return () -> sink.asFlux().doOnNext(v -> { - System.out.println("Hello " + v); - }); - } - - @Bean - public Function, Message> echo() { - return v -> v; - } - } - - @EnableAutoConfiguration - public static class ReactiveConsumerWithConcurrencyFailureConfiguration { - @Bean - public Consumer>> input() { - return flux -> flux.subscribe(System.out::println); - } - } - - @EnableAutoConfiguration - public static class NoEnableBindingConfiguration { - - @Bean - public Function func() { - return x -> { - System.out.println("Function"); - return x; - }; - } - - @Bean - public Function, Flux>> addHeaders() { - return flux -> flux.map(value -> { - return MessageBuilder.withPayload(value).setHeader("foo", "bar").build(); - }); - } - - @Bean - public Function, Flux> aggregate() { - return inbound -> inbound.log().window(Duration.ofSeconds(1)) - .flatMap(w -> w.reduce("", (s1, s2) -> s1 + s2)).log(); - } - - @Bean - public Consumer cons() { - return x -> { - System.out.println("Consumer"); - }; - } - } - - @EnableAutoConfiguration - public static class SingleFunctionConfiguration2 { - - @Bean - public QueueChannel testChannel() { - return new QueueChannel(); - } - - @Bean - public Function imperative() { - return x -> { - return x.getClass().getSimpleName(); - }; - } - - @Bean - public Function, Flux> reactive() { - return flux -> flux.map(x -> { - return x.getClass().getSimpleName(); - }); - } - - @Bean - public Consumer>> reactiveConsumer(MessageChannel testChannel) { - return flux -> flux.subscribe(v -> { - testChannel.send(new GenericMessage(((Message) v).getPayload().getClass().getName())); - }); - } - - @Bean - public Function>, Mono> reactiveFunctionConsumer(MessageChannel testChannel) { - return flux -> flux.doOnNext(x -> { - testChannel.send(new GenericMessage(((Message) x).getPayload().getClass().getName())); - }).then(); - } - } - - @EnableAutoConfiguration - public static class SingleFunctionConfiguration { - - @Bean - public Function func() { - return x -> { - System.out.println("Function"); - return x; - }; - } - } - - @EnableAutoConfiguration - public static class SingleConsumerConfiguration { - - @Bean - public Consumer consumer() { - return value -> { - System.out.println(value); - System.setProperty("consumer", value); - }; - } - } - - @EnableAutoConfiguration - public static class NullMessagerConfiguration { - - @Bean - public Function func() { - return value -> { - assertThat(value).isNull(); - return "NULL"; - }; - } - - @Bean - public Function, String> funcMessage() { - return value -> { - Object v = value.getPayload(); - String className = v.getClass().getName(); - assertThat(className).isEqualTo("org.springframework.kafka.support.KafkaNull"); - return "NULL"; - }; - } - } - - @EnableAutoConfiguration - public static class SingleReactiveConsumerConfiguration { - - @Bean - public Consumer> consumer() { - return flux -> flux.subscribe(value -> { - System.out.println(value); - System.setProperty("consumer", value); - }); - } - } - - @EnableAutoConfiguration - public static class FunctionConsumerCopositionConfiguration { - - @Bean - public Consumer consumer() { - return v -> { - System.out.println("==== Consuming " + v); - System.setProperty("FunctionConsumerCopositionConfiguration", v); - }; - } - - @Bean - public Function echo() { - return v -> { - System.out.println("==> Echo " + v); - return v; - }; - } - } - - @EnableAutoConfiguration - public static class ReactiveFunctionConfiguration { - - @Bean - public Function, Flux> echo() { - return flux -> flux.map(value -> { - System.out.println("echo value reqctive " + value); - if (value.equals("error")) { - throw new RuntimeException("intentional"); - } - return value; - }); - } - - @Bean - public Function>, Flux>> reactivePojoMessage() { - return flux -> flux.map(message -> { - Person p = message.getPayload(); - p.setName(p.getName().toUpperCase()); - return MessageBuilder.withPayload(p).copyHeaders(message.getHeaders()).build(); - }); - } - } - - @EnableAutoConfiguration - public static class FunctionalConsumerConfiguration { - @Bean - public Function, Mono> funcConsumer() { - return flux -> flux.doOnNext(value -> { - System.out.println(value); - System.setProperty("consumer", value); - }).then(); - } - } - - @EnableAutoConfiguration - public static class EmptyConfiguration { - - } - - @EnableAutoConfiguration - public static class SendToDestinationConfiguration { - @Bean - public Function, Flux>> echo() { - return flux -> flux.map(v -> { - return MessageBuilder.withPayload(v).setHeader("spring.cloud.stream.sendto.destination", v).build(); - }); - } - } - - @EnableAutoConfiguration - public static class ImperativeSendToDestinationConfiguration { - @Bean - public Function> echo() { - return s -> MessageBuilder.withPayload(s).setHeader("spring.cloud.stream.sendto.destination", s).build(); - } - } - - @EnableAutoConfiguration - public static class SCF_GH_409Configuration { - - @Bean - public Serializable blah() { - return new Foo(); - } - - @SuppressWarnings("serial") - private static class Foo implements Supplier, Serializable { - - @Override - public Object get() { - // TODO Auto-generated method stub - return null; - } - - } - } - - @EnableAutoConfiguration - public static class FunctionSampleSpringIntegrationConfiguration { - - @Bean - public IntegrationFlow uppercaseFlow() { - return IntegrationFlow.from(MessageFunction.class, gateway -> gateway.beanName("uppercase")) - .transform(String::toUpperCase).get(); - } - - } - - public interface MessageFunction extends Function, Message> { - - } - - @EnableAutoConfiguration - public static class SupplierWithExplicitPollerConfiguration { - - @Bean - public Supplier supplier() { - return () -> "hello"; - } - } - - @EnableAutoConfiguration - public static class ImperativeSupplierComposedWithReactiveFunctionConfiguration { - - @Bean - public Supplier supplier() { - return () -> System.currentTimeMillis(); - } - - @Bean - public Function, Flux> functionA() { - return flux -> flux; - } - - } - - @EnableAutoConfiguration(exclude = ContextFunctionCatalogAutoConfiguration.class) - public static class NoFunctionEnabledConfiguration { - - @Bean - public Supplier supplier() { - return () -> "hello"; - } - } - - @EnableAutoConfiguration - public static class CompositionWithCollectionConfiguration { - - @Bean - public Function>>, Message>>> funcA() { - return v -> v; - } - - @Bean - public Function>>, Message>>> funcB() { - return v -> { - return v; - }; - } - } - - @EnableAutoConfiguration - public static class SplittableTypesConfiguration { - - @Bean - public Function>> funcCollectionOfMessages() { - return v -> { - String[] values = v.split(","); - List> messages = new ArrayList<>(); - for (String value : values) { - messages.add(MessageBuilder.withPayload(value).build()); - } - return messages; - }; - } - - @SuppressWarnings("unchecked") - @Bean - public Function[]> funcArrayOfMessages() { - return v -> { - String[] values = v.split(","); - List> messages = new ArrayList<>(); - for (String value : values) { - messages.add(MessageBuilder.withPayload(value).build()); - } - return messages.toArray(new Message[0]); - }; - } - } - - @EnableAutoConfiguration - public static class PojoFunctionConfiguration { - - @Bean - public Function echoPerson() { - return x -> x; - } - - @Bean - public FunctionAroundWrapper faw() { - return new FunctionAroundWrapper() { - - @Override - protected Object doApply(Object input, FunctionInvocationWrapper targetFunction) { - return targetFunction.apply(input); - } - }; - } - - @Bean - public Supplier personSupplier() { - Person p = new Person(); - p.setId(21); - p.setName("Jim Lehey"); - return () -> p; - } - - @Bean - public Supplier> reactivePersonSupplier() { - return () -> { - Person p = new Person(); - p.setId(21); - p.setName("Jim Lehey"); - return Flux.just(p); - }; - } - - @Bean - public Function func() { - return x -> { - Person person = new Person(); - person.setName(x); - person.setId(3); - return person; - }; - } - - @Bean - public Function uppercase() { - return v -> v.toUpperCase(); - } - - @Bean - public Function>> persons() { - return x -> { - Person person = new Person(); - person.setName(x); - person.setId(3); - return Collections.singletonList(MessageBuilder.withPayload(person).build()); - }; - } - - @Bean - public Function, Flux> funcReactive() { - return flux -> flux.map(value -> { - Person person = new Person(); - person.setName(value); - person.setId(3); - return person; - }); - } - } - - @EnableAutoConfiguration - public static class ExplicitChannelConfiguration { - - @Bean("echo-in-0") - public SubscribableChannel myChannel() { - return new DirectWithAttributesChannel(); - } - - @Bean - public Function echo() { - return x -> x; - } - } - - @EnableAutoConfiguration - public static class FailedApplicationListenerConfiguration { - - @Bean - public Function echo() { - return x -> x; - } - - @Bean - public ApplicationListener bindingCreatedEventListener() { - return bindingCreatedEvent -> { - throw new RuntimeException("Test"); - }; - } - } - - @EnableAutoConfiguration - public static class SingleConsumerWithMultipleDestinationConfiguration { - - @Bean - public Consumer consumerMultiple() { - return value -> { - System.out.println(value); - }; - } - } - - @EnableAutoConfiguration - public static class WrappedBiConsumerAutoConfiguration { - - @Bean - public BiConsumer> testBiConsumer() { - return (a, b) -> { }; - } - - } - - public static class Person { - private String name; - private int id; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/MultipleInputOutputFunctionTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/MultipleInputOutputFunctionTests.java deleted file mode 100644 index 821e98897..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/MultipleInputOutputFunctionTests.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function; - -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.UnicastProcessor; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.AbstractMessageConverter; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * - * @author Oleg Zhurakousky - * - */ -public class MultipleInputOutputFunctionTests { - - @Test - void testFailureWithNonReactiveFunction() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multipleInputNonReactive")); - } - - @Test - void testFailureWithReactiveArrayOutput() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutput")); - } - - @Test - void testFailureWithReactiveArrayOutputNonGeneric() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutputNoGeneric")); - } - - @Test - void testFailureWithReactiveArrayInput() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=genericReactiveArrayInput")); - } - - @Test - void testFailureWithReactiveArrayInputNonGeneric() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=nonGenericReactiveArrayInput")); - } - - @Test - void testFailureWithConsumer() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiInputConsumer")); - } - - @Test - void testMultiInputSingleOutput() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiInputSingleOutput")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build(); - Message integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build(); - inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0"); - inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1"); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes()); - outputMessage = outputDestination.receive(0, "multiInputSingleOutput-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes()); - } - } - - @Test - void testMultiInputMessageSingleOutput() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiInputSingleOutputMessage")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build(); - Message integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build(); - inputDestination.send(stringInputMessage, "multiInputSingleOutputMessage-in-0"); - inputDestination.send(integerInputMessage, "multiInputSingleOutputMessage-in-1"); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes()); - outputMessage = outputDestination.receive(0, "multiInputSingleOutputMessage-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes()); - } - } - - @Test - void testSingleInputMultiOutput() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=singleInputMultipleOutputs", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-0.producer.partitionKeyExpression=payload", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-0.producer.partitionCount=10", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-1.producer.partitionKeyExpression=payload", - "--spring.cloud.stream.bindings.singleInputMultipleOutputs-out-1.producer.partitionCount=10")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - for (int i = 0; i < 10; i++) { - inputDestination.send(MessageBuilder.withPayload(String.valueOf(i).getBytes()).build(), "singleInputMultipleOutputs-in-0"); - } - - int counter = 0; - for (int i = 0; i < 5; i++) { - Message even = outputDestination.receive(0, "singleInputMultipleOutputs-out-0"); - assertThat(even.getPayload()).isEqualTo(("EVEN: " + String.valueOf(counter++)).getBytes()); - Message odd = outputDestination.receive(0, "singleInputMultipleOutputs-out-1"); - assertThat(odd.getPayload()).isEqualTo(("ODD: " + String.valueOf(counter++)).getBytes()); - } - } - } - - @Test - void testMultipleFunctions() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=uppercase;reverse")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage, "uppercase-in-0"); - inputDestination.send(inputMessage, "reverse-in-0"); - - Message outputMessage = outputDestination.receive(0, "uppercase-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes()); - - outputMessage = outputDestination.receive(0, "reverse-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("olleH".getBytes()); - } - } - - @Test - void testMultipleFunctionsWithComposition() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ReactiveFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=uppercase|reverse;reverse|uppercase")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage, "uppercasereverse-in-0"); - inputDestination.send(inputMessage, "reverseuppercase-in-0"); - - Message outputMessage = outputDestination.receive(0, "uppercasereverse-out-0"); - System.out.println(new String(outputMessage.getPayload())); - assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes()); - - outputMessage = outputDestination.receive(0, "reverseuppercase-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes()); - } - } - - @Test - void testMultiInputSingleOutputWithCustomContentType() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ContentTypeConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiInputSingleOutput", - "--spring.cloud.stream.bindings.multiInputSingleOutput-in-0.content-type=string/person", - "--spring.cloud.stream.bindings.multiInputSingleOutput-in-1.content-type=string/employee")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build(); - Message integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build(); - inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0"); - inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1"); - - Message outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("RICKY".getBytes()); - outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("BOBBY".getBytes()); - } - } - - @Test - void testMultiInputSingleOutputWithCustomContentType2() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - ContentTypeConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=multiInputSingleOutput2", - "--spring.cloud.stream.bindings.multiInputSingleOutput2-in-0.content-type=string/person", - "--spring.cloud.stream.bindings.multiInputSingleOutput2-in-1.content-type=string/employee", - "--spring.cloud.stream.bindings.multiInputSingleOutput2-out-0.content-type=string/person")) { - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - Message stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build(); - Message integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build(); - inputDestination.send(stringInputMessage, "multiInputSingleOutput2-in-0"); - inputDestination.send(integerInputMessage, "multiInputSingleOutput2-in-1"); - - Message outputMessage = outputDestination.receive(1000, "multiInputSingleOutput2-out-0"); - assertThat(outputMessage.getPayload()).isEqualTo("rickybobby".getBytes()); - } - } - - @EnableAutoConfiguration - public static class ReactiveFunctionConfiguration { - - @Bean - public Function uppercase() { - return value -> value.toUpperCase(); - } - - @Bean - public Function reverse() { - return value -> new StringBuilder(value).reverse().toString(); - } - - @Bean - public Function, String> multipleInputNonReactive() { // not supported - return tuple -> null; - } - - @Bean - public Function, Flux>, Flux[]> multiReactiveInputReactiveArrayOutput() { // not supported - return tuple -> null; - } - - @Bean - public Consumer, Flux>> multiInputConsumer() { // not supported - return tuple -> System.out.println(); - } - - @SuppressWarnings("rawtypes") - @Bean - public Function, Flux>, Flux[]> multiReactiveInputReactiveArrayOutputNoGeneric() { // not supported - return tuple -> null; - } - - @Bean - public Function[], Tuple2, Flux>> genericReactiveArrayInput() { // not supported - return tuple -> null; - } - - @SuppressWarnings("rawtypes") - @Bean - public Function, Flux>> nonGenericReactiveArrayInput() { // not supported - return tuple -> null; - } - - @Bean - public Function, Flux>, Flux> multiInputSingleOutput() { - return tuple -> { - Flux stringStream = tuple.getT1(); - Flux intStream = tuple.getT2().map(i -> String.valueOf(i)); - return Flux.merge(stringStream, intStream); - }; - } - - @Bean - public Function>, Flux>>, Flux> multiInputSingleOutputMessage() { - return tuple -> { - Flux stringStream = tuple.getT1().map(m -> m.getPayload()); - Flux intStream = tuple.getT2().map(i -> { - int v = i.getPayload(); - return String.valueOf(v); - }); - return Flux.merge(stringStream, intStream); - }; - } - - @Bean - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static Function, Tuple2, Flux>> singleInputMultipleOutputs() { - return flux -> { - Flux connectedFlux = flux.publish().autoConnect(2); - UnicastProcessor even = UnicastProcessor.create(); - UnicastProcessor odd = UnicastProcessor.create(); - Flux evenFlux = connectedFlux.filter(number -> number % 2 == 0).doOnNext(number -> even.onNext("EVEN: " + number)); - Flux oddFlux = connectedFlux.filter(number -> number % 2 != 0).doOnNext(number -> odd.onNext("ODD: " + number)); - - return Tuples.of(Flux.from(even).doOnSubscribe(x -> evenFlux.subscribe()), Flux.from(odd).doOnSubscribe(x -> oddFlux.subscribe())); - }; - } - } - - @EnableAutoConfiguration - public static class ContentTypeConfiguration { - - @Bean - public Function, Flux>, Flux> multiInputSingleOutput() { - return tuple -> { - Flux stringStream = tuple.getT1().map(p -> p.getName().toUpperCase()); - Flux intStream = tuple.getT2().map(p -> p.getName().toUpperCase()); - return Flux.merge(stringStream, intStream); - }; - } - - @Bean - public Function, Flux>, Flux> multiInputSingleOutput2() { - return tuple -> { - return Flux.merge(tuple.getT1(), tuple.getT2()).buffer(Duration.ofMillis(1000)).map(list -> { - String personName = ((Person) list.get(0)).getName(); - String employeeName = ((Employee) list.get(1)).getName(); - return new Person(personName + employeeName); - }); - }; - } - - @Bean - public MessageConverter stringToPersonConverter() { - return new AbstractMessageConverter(MimeType.valueOf("string/person")) { - - @Override - protected boolean supports(Class clazz) { - return Person.class.isAssignableFrom(clazz); - } - - @Override - @Nullable - protected Object convertFromInternal( - Message message, Class targetClass, @Nullable Object conversionHint) { - String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8); - Person person = new Person(name); - return person; - } - - @Override - @Nullable - protected Object convertToInternal( - Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { - - return ((Person) payload).getName().getBytes(StandardCharsets.UTF_8); - } - }; - } - - @Bean - public MessageConverter stringToEmployeeConverter() { - return new AbstractMessageConverter(MimeType.valueOf("string/employee")) { - - @Override - protected boolean supports(Class clazz) { - return Employee.class.isAssignableFrom(clazz); - } - - @Override - @Nullable - protected Object convertFromInternal( - Message message, Class targetClass, @Nullable Object conversionHint) { - String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8); - Employee person = new Employee(name); - return person; - } - - @Override - @Nullable - protected Object convertToInternal( - Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { - - return ((Employee) payload).getName().getBytes(StandardCharsets.UTF_8); - } - }; - } - } - - private static class Person { - private final String name; - - Person(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - private static class Employee { - private final String name; - - Employee(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/PollableSourceTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/PollableSourceTests.java deleted file mode 100644 index 5dd8f6fca..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/PollableSourceTests.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.function; - -import java.time.Duration; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.PollableMessageSource; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.integration.scheduling.PollerMetadata; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.scheduling.support.CronTrigger; -import org.springframework.scheduling.support.PeriodicTrigger; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - - - -/** - * - * @author Oleg Zhurakousky - * @author Artem Bilan - * - */ -public class PollableSourceTests { - - @Test - void testPollableSource() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PollableAppSampleConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false", - "--spring.cloud.stream.pollable-source=blah", - "--spring.integration.poller.cron=*/2 * * * * *", - "--spring.cloud.stream.poller.max-messages-per-poll=4")) { - - - DefaultPollableMessageSource pollableSource = (DefaultPollableMessageSource) context.getBean(PollableMessageSource.class); - pollableSource.poll(message -> assertThat(message.getPayload()).isNotNull()); - - PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class); - assertThat(pollerMetadata.getTrigger()).isInstanceOf(CronTrigger.class); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "expression.expression")) - .isEqualTo("*/2 * * * * *"); - assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(4); - } - } - - @Test - void testPollerDefaultFixedDelay() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PollableAppSampleConfiguration.class)) - .web(WebApplicationType.NONE).run()) { - - PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class); - assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(false); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofSeconds(1)); - assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(1); - } - } - - @Test - void testPollerProvidedFixedDelayAndMaxMessagesPerPoll() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PollableAppSampleConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.integration.poller.fixed-delay=7s", - "--spring.integration.poller.max-messages-per-poll=13")) { - - PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class); - assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(false); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofSeconds(7)); - assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(13); - } - } - - @Test - void testNoPollerFixedDelayIfFixedRatePresent() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PollableAppSampleConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.integration.poller.fixed-rate=200")) { - - PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class); - assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(true); - assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofMillis(200)); - assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(1); - } - } - - @Test - void testPollerMutualProperties() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(PollableAppSampleConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.integration.poller.fixed-rate=200", - "--spring.cloud.stream.poller.fixed-delay=300")) - .withRootCauseExactlyInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class) - .withMessageContaining("are mutually exclusive"); - } - - @EnableAutoConfiguration - @Configuration - public static class PollableAppSampleConfiguration { - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/RoutingFunctionTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/RoutingFunctionTests.java deleted file mode 100644 index 71c0c0818..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/RoutingFunctionTests.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.function.context.FunctionProperties; -import org.springframework.cloud.function.context.config.RoutingFunction; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinder; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.channel.AbstractSubscribableChannel; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.util.MimeTypeUtils; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Oleg Zhurakousky - * @since 2.2.1 - */ -public class RoutingFunctionTests { - - - @BeforeAll - public static void before() { - System.getProperties().remove("spring.cloud.function.routing.enabled"); - System.getProperties().remove("spring.cloud.stream.function.definition"); - System.getProperties().remove("spring.cloud.function.definition"); - System.getProperties().remove("spring.cloud.function.routing-expression"); - } - - @Test - void testRoutingViaExplicitEnablingAndDefinitionHeader() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.function.routing.enabled=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()) - .setHeader(FunctionProperties.PREFIX + ".definition", "echo") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - } - } - - @Test - void testRoutingViaExplicitEnablingAndRoutingExpressionProperty() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.routing-expression=headers.contentType.toString().equals('text/plain') ? 'echo' : null", - "--spring.cloud.stream.function.routing.enabled=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - } - } - - @Test - void testRoutingViaExplicitEnablingAndRoutingExpressionHeader() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.function.routing.enabled=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()) - .setHeader("spring.cloud.function.routing-expression", "'echo'") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes()); - } - } - - @Test - void testRoutingViaExplicitDefinitionAndDefinitionHeader() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=" + RoutingFunction.FUNCTION_NAME)) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()) - .setHeader("spring.cloud.function.definition", "echo|uppercase") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes()); - } - } - - @Test - void testDefaultRoutingFunctionBindingFlux() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.stream.function.routing.enabled=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()) - .setHeader("spring.cloud.function.definition", "echoFlux") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build(); - inputDestination.send(inputMessage); - - TestChannelBinder binder = context.getBean(TestChannelBinder.class); - Throwable ex = ((Exception) binder.getLastError().getPayload()).getCause(); - assertThat(ex).isInstanceOf(IllegalStateException.class); - assertThat(ex.getMessage()).isEqualTo("Routing to functions that return Publisher is not supported in the context of Spring Cloud Stream."); - } - } - - - @Test - void testPojoFunction() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=" + RoutingFunction.FUNCTION_NAME)) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("{\"name\":\"bob\"}".getBytes()) - .setHeader("spring.cloud.function.definition", "pojoecho") - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("{\"name\":\"bob\"}".getBytes()); - - } - } - - @Test - void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledExplicitly() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME, - "--spring.cloud.stream.function.routing.enabled=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes()); - } - } - - @Test - void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledImplicitly() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME)) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes()); - } - } - - @Test - void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledExplicitlyAndMoreComposition() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME + "|reverse", - "--spring.cloud.stream.function.routing.enabled=true")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()) - .build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes()); - } - } - - @Test - void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledImplicitlyAndMoreComposition() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingFunctionConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME + "|reverse")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - OutputDestination outputDestination = context - .getBean(OutputDestination.class); - - Message inputMessage = MessageBuilder - .withPayload("Hello".getBytes()).build(); - inputDestination.send(inputMessage); - - Message outputMessage = outputDestination.receive(); - assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes()); - } - } - - @SuppressWarnings("unchecked") - @Test - void testRoutingToConsumers() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - RoutingConsumerConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.routing-expression=headers['func_name']")) { - - InputDestination inputDestination = context.getBean(InputDestination.class); - Message inputMessage = MessageBuilder - .withPayload("foo".getBytes()) - .setHeader("func_name", "consume") - .build(); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Field chField = ReflectionUtils.findField(outputDestination.getClass(), "channels"); - chField.setAccessible(true); - List outputChannels = (List) chField.get(outputDestination); - assertThat(outputChannels.isEmpty()); - inputDestination.send(inputMessage); - assertThat(outputChannels.isEmpty()); - inputMessage = MessageBuilder - .withPayload("foo".getBytes()) - .setHeader("func_name", "echo") - .build(); - inputDestination.send(inputMessage); - assertThat(outputChannels.size()).isEqualTo(1); - } - } - - @EnableAutoConfiguration - public static class RoutingConsumerConfiguration { - @Bean - public Consumer consume() { - return System.out::println; - } - @Bean - public Function echo() { - return x -> x; - } - } - - @EnableAutoConfiguration - public static class RoutingFunctionConfiguration { - - @Bean - public Function echo() { - return x -> { - System.out.println("===> echo"); - return x; - }; - } - - @Bean - public Function pojoecho() { - return x -> { - System.out.println("===> pojoecho"); - return x; - }; - } - - @Bean - public Function, Flux> echoFlux() { - return flux -> flux.map(x -> { - System.out.println("===> echoFlux"); - return x; - }); - } - - @Bean - public Function, Message> enrich() { - return x -> { - System.out.println("===> enrich"); - return MessageBuilder.withPayload(x.getPayload()).setHeader("spring.cloud.function.definition", "uppercase").build(); - }; - } - - @Bean - public Function uppercase() { - return x -> { - System.out.println("===> uppercase"); - return x.toUpperCase(); - }; - } - - @Bean - public Function reverse() { - return x -> { - System.out.println("===> reverse"); - return new StringBuilder(x).reverse().toString(); - }; - } - } - - @SuppressWarnings("unused") - private static class Person { - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/ScenarioTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/ScenarioTests.java deleted file mode 100644 index ba5ba3619..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/ScenarioTests.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.function; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Oleg Zhurakousky - * - */ -public class ScenarioTests { - - @Test - void test2106() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(ConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=consume;echo", - "--spring.cloud.stream.bindings.consume-in-0.destination=input", - "--spring.cloud.stream.bindings.echo-in-0.destination=echoin", - "--spring.cloud.stream.bindings.echo-out-0.destination=echoout", - "--spring.jmx.enabled=false")) { - - ConsumerConfiguration configuration = context.getBean(ConsumerConfiguration.class); - - OutputDestination output = context.getBean(OutputDestination.class); - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("input", "destination"); - bridge.send("input", "destination"); - bridge.send("input", "destination"); - - bridge.send("consume-in-0", "hello"); - bridge.send("consume-in-0", "hello"); - bridge.send("consume-in-0", "hello"); - - bridge.send("echoin", "hello"); - bridge.send("echoin", "hello"); - bridge.send("echoin", "hello"); - - assertThat(configuration.destinationCounter).isEqualTo(3); - assertThat(configuration.bindingCounter).isEqualTo(3); - - assertThat(output.receive(1000, "echoout")).isNotNull(); - assertThat(output.receive(1000, "echoout")).isNotNull(); - assertThat(output.receive(1000, "echoout")).isNotNull(); - assertThat(output.receive(1000, "echoout")).isNull(); - } - } - - @Test - void testComposingSupplierWuthTypelessMessageFunction() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SupplierConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=messageSupplier|messageFunction")) { - - OutputDestination output = context.getBean(OutputDestination.class); - assertThat(output.receive(1000, "messageSuppliermessageFunction-out-0")).isNotNull(); - assertThat(output.receive(1200, "messageSuppliermessageFunction-out-0")).isNotNull(); - assertThat(output.receive(1300, "messageSuppliermessageFunction-out-0")).isNotNull(); - } - } - - @Test - void test2107() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(FunctionReturningNullConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=uppercase")) { - - InputDestination input = context.getBean(InputDestination.class); - input.send(new GenericMessage("a".getBytes()), "uppercase-in-0"); - OutputDestination output = context.getBean(OutputDestination.class); - assertThat(new String(output.receive(2000, "uppercase-out-0").getPayload())).isEqualTo("a"); - input.send(new GenericMessage("b".getBytes()), "uppercase-in-0"); - assertThat(output.receive(2000, "uppercase-out-0")).isNull(); - } - } - - @Test - void test2113() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(TestConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=genericTypeFunction")) { - - InputDestination input = context.getBean(InputDestination.class); - OutputDestination output = context.getBean(OutputDestination.class); - - input.send(new GenericMessage("hello".getBytes()), "genericTypeFunction-in-0"); - assertThat(new String(output.receive(1000, "genericTypeFunction-out-0").getPayload())).isEqualTo("hello_hello"); - } - } - - - - @EnableAutoConfiguration - @Configuration - public static class TestConfiguration { - @SuppressWarnings("unchecked") - @Bean - public Function genericTypeFunction() { - return v -> { - return (O) ("hello_" + new String((byte[]) v)); - }; - } - } - - @EnableAutoConfiguration - @Configuration - public static class SupplierConfiguration { - @Bean - public Supplier> messageSupplier() { - return () -> new GenericMessage<>("10/27/20 07:20:01"); - } - @Bean - public Function, Message> messageFunction() { - return message -> { - return message; - }; - } - - } - - @EnableAutoConfiguration - @Configuration - public static class FunctionReturningNullConfiguration { - @Bean - public Function uppercase() { - return v -> { - if ("a".equals(v)) { - return v; - } - else { - return null; - } - }; - } - } - - @EnableAutoConfiguration - public static class ConsumerConfiguration { - - private int destinationCounter; - - private int bindingCounter; - - @Bean - public Consumer consume() { - return v -> { - if (v.equals("destination")) { - destinationCounter++; - } - else { - bindingCounter++; - } - }; - } - - @Bean - public Function echo() { - return v -> v; - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/StreamBridgeTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/StreamBridgeTests.java deleted file mode 100644 index 5428a4c78..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/StreamBridgeTests.java +++ /dev/null @@ -1,885 +0,0 @@ -/* - * Copyright 2020-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.cloud.stream.function; - -import java.lang.reflect.Field; -import java.net.URI; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.function.cloudevent.CloudEventMessageBuilder; -import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.function.context.message.MessageUtils; -import org.springframework.cloud.stream.binder.test.InputDestination; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.binding.NewDestinationBindingCallback; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.config.GlobalChannelInterceptor; -import org.springframework.integration.dsl.IntegrationFlow; -import org.springframework.integration.handler.LoggingHandler; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.AbstractMessageConverter; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * - */ -public class StreamBridgeTests { - - @BeforeAll - public static void before() { - System.clearProperty("spring.cloud.function.definition"); - } - - @Test // see SCF-985 - void ensurePassThruFunctionIsNotPrePostProcessed() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - StreamBridge streamBridge = context.getBean(StreamBridge.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = CloudEventMessageBuilder.withData("foo") // cloud event - .setType("CustomType").setSource("CustomSource").setSubject("CustomSubject").build(); - streamBridge.send("fooDestination", message); - - Message messageReceived = outputDestination.receive(1000, "fooDestination"); - assertThat(CloudEventMessageUtils.getSource(messageReceived)).isEqualTo(URI.create("CustomSource")); - assertThat(CloudEventMessageUtils.getSubject(messageReceived)).isEqualTo("CustomSubject"); - assertThat(CloudEventMessageUtils.getType(messageReceived)).isEqualTo("CustomType"); - } - } - - @Test // - void validateWARNifSendingToINputBinding() { // GH-2563 - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SimpleConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=echo", - "--spring.cloud.stream.function.bindings.echo-in-0=input")) { - - StreamBridge streamBridge = context.getBean(StreamBridge.class); - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - streamBridge.send("input", "foo"); - // just validate that there is a WARN message about sending to the input binding - } - } - - @Test - void extractStreamBridgeAsStreamOperations() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - StreamOperations streamOperations = context.getBean(StreamOperations.class); - StreamBridge streamBridge = context.getBean(StreamBridge.class); - assertThat(streamOperations).isSameAs(streamBridge); - } - } - - @Test - void test_SCF_856() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - StreamBridge streamBridge = context.getBean(StreamBridge.class); - streamBridge.send("myBinding-out-0", - CloudEventMessageBuilder.withData("hello").setSource("my-source") - .setId(UUID.randomUUID().toString()).setSpecVersion("1.0").setType("myType") - .setHeader(MessageUtils.TARGET_PROTOCOL, "kafka").build(), - MimeTypeUtils.APPLICATION_JSON); - OutputDestination output = context.getBean(OutputDestination.class); - Message result = output.receive(); - assertThat(result.getHeaders().get("ce_type")).isNotNull(); - assertThat(result.getHeaders().get("ce_source")).isNotNull(); - } - } - - /* - * This test must not result in exception stating "Partition key cannot be null" - * See https://github.com/spring-cloud/spring-cloud-stream/issues/2249 for more details - */ - @Test - void test_2249() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - EmptyConfiguration.class)).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.source=outputA;outputB", - "--spring.cloud.stream.bindings.outputA-out-0.producer.partition-count=3", - "--spring.cloud.stream.bindings.outputA-out-0.producer.partition-key-expression=headers['partitionKey']", - "--spring.cloud.stream.bindings.outputB-out-0.destination=outputB", - "--spring.cloud.stream.bindings.outputB-out-0.producer.partition-count=3", - "--spring.jmx.enabled=false")) { - StreamBridge streamBridge = context.getBean(StreamBridge.class); - streamBridge.send("outputA-out-0", MessageBuilder.withPayload("A").setHeader("partitionKey", "A").build()); - streamBridge.send("outputB", MessageBuilder.withPayload("B").build()); - streamBridge.send("outputA-out-0", MessageBuilder.withPayload("C").setHeader("partitionKey", "C").build()); - streamBridge.send("outputB", MessageBuilder.withPayload("D").build()); - - OutputDestination output = context.getBean(OutputDestination.class); - assertThat(output.receive(1000, "outputA-out-0").getHeaders().containsKey("scst_partition")).isTrue(); - assertThat(output.receive(1000, "outputB").getHeaders().containsKey("scst_partition")).isFalse(); - assertThat(output.receive(1000, "outputA-out-0").getHeaders().containsKey("scst_partition")).isTrue(); - assertThat(output.receive(1000, "outputB").getHeaders().containsKey("scst_partition")).isFalse(); - } - } - - @SuppressWarnings("rawtypes") - @Test - void test_2785() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration( - EmptyConfiguration.class)).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.source=outputA;outputB", - "--spring.cloud.stream.bindings.outputA-out-0.producer.use-native-encoding=false", - "--spring.cloud.stream.bindings.outputB-out-0.producer.partition-count=1", - "--spring.cloud.stream.bindings.outputC-out-0.producer.use-native-encoding=true", - "--spring.cloud.stream.bindings.outputD-out-0.content-type=text/html", - "--spring.jmx.enabled=false")) { - StreamBridge streamBridge = context.getBean(StreamBridge.class); - Field field = ReflectionUtils.findField(StreamBridge.class, "streamBridgeFunctionCache"); - field.setAccessible(true); - Map functionCache = (Map) field.get(streamBridge); - - streamBridge.send("outputA-out-0", MessageBuilder.withPayload("A").build()); - assertThat(functionCache.size()).isEqualTo(1); - streamBridge.send("foo", MessageBuilder.withPayload("A").build()); - assertThat(functionCache.size()).isEqualTo(1); - streamBridge.send("outputB-out-0", MessageBuilder.withPayload("A").build()); - assertThat(functionCache.size()).isEqualTo(1); - streamBridge.send("outputC-out-0", MessageBuilder.withPayload("A").build()); - assertThat(functionCache.size()).isEqualTo(2); - streamBridge.send("outputD-out-0", MessageBuilder.withPayload("A").build()); - assertThat(functionCache.size()).isEqualTo(3); - } - } - - /* - * This test verifies that when a partition key expression is set, then scst_partition is always set, even in - * concurrent scenarios. - * See https://github.com/spring-cloud/spring-cloud-stream/issues/2299 for more details - */ - @Test - void test_2299_scstPartitionAlwaysSetEvenInConcurrentScenarios() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)).web( - WebApplicationType.NONE).run("--spring.cloud.stream.source=outputA", - "--spring.cloud.stream.bindings.outputA-out-0.producer.partition-count=3", - "--spring.cloud.stream.bindings.outputA-out-0.producer.partition-key-expression=headers['partitionKey']", - "--spring.jmx.enabled=false")) { - StreamBridge streamBridge = context.getBean(StreamBridge.class); - - int threadCount = 10; - Set threads = IntStream.range(0, threadCount) - .mapToObj(i -> (Runnable) () -> IntStream.range(0, 100).forEach(j -> { - String value = "M-" + i + "-" + j; - streamBridge.send("outputA-out-0", - MessageBuilder.withPayload(value).setHeader("partitionKey", value).build()); - })).map(Thread::new).collect(Collectors.toSet()); - - threads.forEach(Thread::start); - for (Thread thread : threads) { - thread.join(); - } - - int messagesWithoutScstPartition = 0; - OutputDestination output = context.getBean(OutputDestination.class); - Message message = output.receive(1000, "outputA-out-0"); - while (message != null) { - if (!message.getHeaders().containsKey("scst_partition")) { - messagesWithoutScstPartition++; - } - message = output.receive(1000, "outputA-out-0"); - } - assertThat(messagesWithoutScstPartition).isEqualTo(0); - } - } - - @Test - void testWithOutputContentTypeWildCardBindings() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(ConsumerConfiguration.class, EmptyConfigurationWithCustomConverters.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.stream.bindings.foo.content-type=application/*+foo ", - "--spring.cloud.stream.bindings.bar.content-type=application/*+non-registered-foo", - "--spring.jmx.enabled=false")) { - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("foo", "hello foo"); - bridge.send("bar", "hello bar"); - - OutputDestination output = context.getBean(OutputDestination.class); - - assertThat(output.receive(1000, "foo").getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeType.valueOf("application/json+foo")); - assertThat(output.receive(1000, "bar").getHeaders().get(MessageHeaders.CONTENT_TYPE)) - .isEqualTo(MimeType.valueOf("application/blahblah+non-registered-foo")); - - } - } - - @SuppressWarnings("unchecked") - @Test - void testNoCachingOfStreamBridgeFunction() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(ConsumerConfiguration.class, InterceptorConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=function", - "--spring.jmx.enabled=false")) { - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.TEXT_PLAIN); - bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.APPLICATION_JSON); - bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.TEXT_HTML); - - Field field = ReflectionUtils.findField(StreamBridge.class, "streamBridgeFunctionCache"); - field.setAccessible(true); - Map map = (Map) field.get(bridge); - assertThat(map.size()).isEqualTo(3); - } - } - - @Test - void testDelayedSend() { - ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(ConsumerConfiguration.class, EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - executor.schedule(() -> bridge.send("blah", "hello foo"), 5000, TimeUnit.MILLISECONDS); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(10000, "blah"); - assertThat(message).isNotNull(); - assertThat(new String(message.getPayload())).isEqualTo("hello foo"); - } - finally { - executor.shutdownNow(); - } - } - - @Test - void testWithInterceptorsMatchedAgainstAllPatterns() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(ConsumerConfiguration.class, InterceptorConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=function", - "--spring.jmx.enabled=false")) { - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("function-in-0", "hello foo"); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(100, "function-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("hello foo"); - assertThat(message.getHeaders().get("intercepted")).isEqualTo("true"); - } - } - - @Test // validate that there is no exception thrown when sending to null channel - void test_2268() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(InterceptorConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false")) { - StreamBridge bridge = context.getBean(StreamBridge.class); - - bridge.send("nullChannel", "blah"); - } - } - - @Test - void testInterceptorIsNotAddedMultipleTimesToTheMessageChannel() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(InterceptorConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false", - "--spring.cloud.stream.dynamic-destination-cache-size=1", - "--spring.cloud.stream.output-bindings=outputA;outputB" - )) { - StreamBridge bridge = context.getBean(StreamBridge.class); - - bridge.send("outputA", "hello foo"); - bridge.send("outputA", "hello foo"); - bridge.send("outputA", "hello foo"); - bridge.send("outputA", "hello foo"); - - AbstractMessageChannel messageChannel = context.getBean("outputA", AbstractMessageChannel.class); - - assertThat(messageChannel.getInterceptors()).hasSize(1); - } - } - - @Test - void testBindingsAreRemovedWithCache() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(InterceptorConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false", - "--spring.cloud.stream.dynamic-destination-cache-size=1" - )) { - StreamBridge bridge = context.getBean(StreamBridge.class); - - bridge.send("a", "hello foo"); - bridge.send("b", "hello foo"); - bridge.send("c", "hello foo"); - bridge.send("d", "hello foo"); - - BindingService bindingService = context.getBean(BindingService.class); - assertThat(bindingService.getProducerBindingNames().length).isEqualTo(1); - assertThat(bindingService.getProducerBindingNames()[0]).isEqualTo("d"); - } - } - - @Test - void testWithInterceptorsRegisteredOnlyOnOutputChannel() throws InterruptedException { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(GH2180Configuration.class)) - .web(WebApplicationType.NONE).run( - "--spring.jmx.enabled=false")) { - - MessageChannel inputChannel = context.getBean("inputChannel", MessageChannel.class); - inputChannel.send(MessageBuilder.withPayload("hello foo").build()); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(100, "outgoing-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("hello foo"); - assertThat(message.getHeaders().get("intercepted")).isEqualTo("true"); - //Ensure that the LoggingHandler in the first SI flow is invoked. - GH2180Configuration.LATCH1.await(10, TimeUnit.SECONDS); - //Ensure that the second SI flow does not trigger its LoggingHandler (aka wiretap/interceptor). - assertThat(GH2180Configuration.LATCH2.getCount()).isEqualTo(1); - } - } - - @Test - void testBindingPropertiesAreHonored() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(ConsumerConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=consumer;function", - "--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.foo.destination=function-in-0", - "--spring.cloud.stream.bindings.foo.producer.partitionCount=5", - "--spring.cloud.stream.bindings.foo.consumer.concurrency=2")) { - - BindingServiceProperties bsProperties = context.getBean(BindingServiceProperties.class); - assertThat(bsProperties.getConsumerProperties("foo").getConcurrency()).isEqualTo(2); - assertThat(bsProperties.getProducerProperties("foo").getPartitionCount()).isEqualTo(5); - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("consumer-in-0", "hello foo"); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(100, "function-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("hello foo"); - assertThat(message.getHeaders().get("concurrency")).isEqualTo(2); - assertThat(message.getHeaders().get("partitionCount")).isEqualTo(5); - } - } - - //see https://github.com/spring-cloud/spring-cloud-function/issues/573 for more details - @Test - void testBridgeActivationWhenFunctionDefinitionIsPresent() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(SimpleConfiguration.class)) - .web(WebApplicationType.NONE).run( - "--spring.cloud.function.definition=echo;uppercase", - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("echo-in-0", "hello foo"); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - assertThat(new String(outputDestination.receive(100, "echo-out-0").getPayload())).isEqualTo("hello foo"); - } - } - - @Test - void testNoBridgeIfNoSourcePropertyDefined() { - assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> { - ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration()) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false"); - context.getBean(StreamBridge.class); - }); - } - - @Test - void testBridgeFunctions() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar", - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("foo-out-0", "hello foo"); - bridge.send("bar-out-0", "hello bar"); - - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - assertThat(new String(outputDestination.receive(100, "foo-out-0").getPayload())).isEqualTo("hello foo"); - assertThat(new String(outputDestination.receive(100, "bar-out-0").getPayload())).isEqualTo("hello bar"); - } - } - - @Test - void testBridgeFunctionsSendingMessagePreservingHeaders() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar", - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("foo-out-0", MessageBuilder.withPayload("hello foo").setHeader("foo", "foo").build()); - bridge.send("bar-out-0", MessageBuilder.withPayload("hello bar").setHeader("bar", "bar").build()); - - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - - Message message = outputDestination.receive(100, "foo-out-0"); - assertThat(message.getPayload()).isEqualTo("hello foo".getBytes()); - assertThat(message.getHeaders().get("foo")).isEqualTo("foo"); - - message = outputDestination.receive(100, "bar-out-0"); - assertThat(message.getPayload()).isEqualTo("hello bar".getBytes()); - assertThat(message.getHeaders().get("bar")).isEqualTo("bar"); - } - } - - @Test - void testBridgeFunctionsWitthPartitionInformation() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(EmptyConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar", - "--spring.cloud.stream.bindings.foo.producer.partitionKeyExpression=payload", - "--spring.cloud.stream.bindings.foo.producer.partitionCount=5", - "--spring.cloud.stream.bindings.bar.producer.partitionKeyExpression=payload", - "--spring.cloud.stream.bindings.bar.producer.partitionCount=1", - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("foo", "a"); - bridge.send("bar", "b"); - bridge.send("foo", "c"); - bridge.send("foo", "d"); - bridge.send("bar", "e"); - bridge.send("foo", "f"); - bridge.send("bar", "g"); - - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(100, "foo"); - - assertThat(new String(message.getPayload())).isEqualTo("a"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(2); - - message = outputDestination.receive(100, "foo"); - assertThat(new String(message.getPayload())).isEqualTo("c"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(4); - - message = outputDestination.receive(100, "foo"); - assertThat(new String(message.getPayload())).isEqualTo("d"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0); - - message = outputDestination.receive(100, "bar"); - assertThat(new String(message.getPayload())).isEqualTo("b"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0); - - message = outputDestination.receive(100, "bar"); - assertThat(new String(message.getPayload())).isEqualTo("e"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0); - - message = outputDestination.receive(100, "bar"); - assertThat(new String(message.getPayload())).isEqualTo("g"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0); - - message = outputDestination.receive(100, "foo"); - assertThat(new String(message.getPayload())).isEqualTo("f"); - assertThat(message.getHeaders().get("scst_partition")).isEqualTo(2); - - //assertThat(new String(outputDestination.receive(100, "bar-out-0").getPayload())).isEqualTo("b"); - } - } - - @Test - void testSendingMessageToOutputOfExistingSupplier() throws Exception { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(TestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.stream.output-bindings=supplier;foo", - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("supplier-out-0", "blah"); - bridge.send("foo-out-0", "b"); - - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(100, "foo-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("b"); - message = outputDestination.receive(100, "supplier-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("hello"); - message = outputDestination.receive(100, "supplier-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("blah"); - } - } - - @Test - void testDynamicDestination() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(TestConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("foo-out-0", "b"); - bridge.send("bar", "hello"); - bridge.send("blah", MessageBuilder.withPayload("message").setHeader("foo", "foo").build()); - - Message message = outputDestination.receive(100, "foo-out-0"); - assertThat(new String(message.getPayload())).isEqualTo("b"); - - message = outputDestination.receive(100, "bar"); - assertThat(new String(message.getPayload())).isEqualTo("hello"); - - message = outputDestination.receive(100, "blah"); - assertThat(new String(message.getPayload())).isEqualTo("message"); - } - } - - @Test - void testWithIntegrationFlowBecauseMarcinSaidSo() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(IntegrationFlowConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("foo", "blah"); - - OutputDestination outputDestination = context.getBean(OutputDestination.class); - Message message = outputDestination.receive(100, "output"); - assertThat(new String(message.getPayload())).isEqualTo("BLAH"); - } - } - - @Test - void testNewBindingCallback() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration - .getCompleteConfiguration(BindingCallbackConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.cloud.stream.source=uppercase", - "--spring.jmx.enabled=false")) { - - StreamBridge bridge = context.getBean(StreamBridge.class); - bridge.send("uppercase-in-0", "hello"); - assertThat(context.getBean("callbackVerifier", AtomicBoolean.class)).isTrue(); - } - } - - @Test - void testDynamicProducerDestination() { - System.clearProperty("spring.cloud.function.definition"); - ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(DynamicProducerDestinationConfig.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", - "--spring.cloud.function.definition=uppercase", - "--spring.cloud.stream.bindings.uppercase-in-0.destination=upper" - ); - - InputDestination source = context.getBean(InputDestination.class); - source.send(new GenericMessage<>("John Doe".getBytes()), "upper"); - - OutputDestination target = context.getBean(OutputDestination.class); - Message message = target.receive(5, "dynamicTopic"); - - assertThat(message).isNotNull(); - assertThat(new String(message.getPayload())).isEqualTo("JOHN DOE"); - } - - @EnableAutoConfiguration - public static class DynamicProducerDestinationConfig { - @Bean - public Function, Message> uppercase() { - return msg -> MessageBuilder.withPayload(msg.getPayload().toUpperCase()) - .setHeader("spring.cloud.stream.sendto.destination", "dynamicTopic").build(); - } - } - - @EnableAutoConfiguration - public static class EmptyConfiguration { - - } - - @EnableAutoConfiguration - public static class EmptyConfigurationWithCustomConverters { - - @Bean - public MessageConverter fooConverter() { - return new AbstractMessageConverter(MimeType.valueOf("application/json+foo"), MimeType.valueOf("application/json+blah")) { - @Override - protected boolean supports(Class clazz) { - return true; - } - - @Override - @Nullable - protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { - return message.getPayload(); - } - - @Override - @Nullable - protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { - if (headers.containsKey(MessageHeaders.CONTENT_TYPE) && - (headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+foo") || - headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+blah"))) { - return payload; - } - return null; - } - }; - } - - @Bean - public MessageConverter barConverter() { - return new AbstractMessageConverter(MimeType.valueOf("application/blahblah+non-registered-foo")) { - @Override - protected boolean supports(Class clazz) { - return true; - } - - @Override - @Nullable - protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { - return message.getPayload(); - } - - @Override - @Nullable - protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { - if (headers.containsKey(MessageHeaders.CONTENT_TYPE) && - (headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+non-registered-foo"))) { - return payload; - } - return null; - } - }; - } - } - - @EnableAutoConfiguration - public static class ConsumerConfiguration { - @Bean - public Consumer consumer(StreamBridge bridge, BindingServiceProperties properties) { - return v -> { - BindingServiceProperties p = properties; - bridge.send("foo", v); - }; - } - @Bean - public Function> function(StreamBridge bridge, BindingServiceProperties properties) { - return v -> { - int concurrency = properties.getConsumerProperties("foo").getConcurrency(); - int partitionCount = properties.getProducerProperties("foo").getPartitionCount(); - BindingServiceProperties p = properties; - return MessageBuilder.withPayload(v) - .setHeader("concurrency", concurrency) - .setHeader("partitionCount", partitionCount) - .build(); - }; - } - } - - @EnableAutoConfiguration - public static class InterceptorConfiguration { - @Bean - @GlobalChannelInterceptor(patterns = "*") - public ChannelInterceptor interceptor() { - return new ChannelInterceptor() { - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder.fromMessage(message).setHeader("intercepted", "true").build(); - } - }; - } - } - - @EnableAutoConfiguration - public static class TestConfiguration { - - @Bean - public Supplier supplier() { - return () -> "hello"; - } - } - - @EnableAutoConfiguration - public static class SimpleConfiguration { - - @Bean - public Function echo() { - return v -> v; - } - - @Bean - public Function uppercase() { - return v -> v.toUpperCase(); - } - } - - - @EnableAutoConfiguration - public static class BindingCallbackConfiguration { - - @Bean - public Function echo() { - return v -> v; - } - - @Bean - public Function uppercase() { - return v -> v.toUpperCase(); - } - - @Bean - public AtomicBoolean callbackVerifier() { - return new AtomicBoolean(); - } - - @Bean - public NewDestinationBindingCallback callback(AtomicBoolean callbackVerifier) { - - return (name, channel, props, extended) -> { - callbackVerifier.set(true); - }; - } - } - - @EnableAutoConfiguration - public static class IntegrationFlowConfiguration { - - @Bean - public IntegrationFlow transform(StreamBridge bridge) { - return IntegrationFlow.from("foo").transform(v -> { - String s = new String((byte[]) v); - return s.toUpperCase(); - }) - .handle(v -> bridge.send("output", v)) - .get(); - } - } - - @EnableAutoConfiguration - public static class GH2180Configuration { - - static CountDownLatch LATCH1 = new CountDownLatch(1); - static CountDownLatch LATCH2 = new CountDownLatch(1); - - @Bean - MessageChannel inputChannel() { - return new DirectChannel(); - } - - @Bean - MessageChannel otherInputChannel() { - return new DirectChannel(); - } - - @Bean - public IntegrationFlow someFlow(MessageHandler sendMessage, MessageChannel inputChannel) { - return IntegrationFlow.from(inputChannel) - .log(LoggingHandler.Level.INFO, (m) -> { - LATCH1.countDown(); - return "Going through the first flow: " + m.getPayload(); - }) - .handle(sendMessage) - .get(); - } - - @Bean - public IntegrationFlow someOtherFlow(MessageHandler sendMessage) { - return IntegrationFlow.from(otherInputChannel()) - .log(LoggingHandler.Level.INFO, (m) -> { - LATCH2.countDown(); - return "Going through the second flow: " + m.getPayload(); - }) - .handle(sendMessage) - .get(); - } - - @Bean - @GlobalChannelInterceptor(patterns = "outgoing-*") - public ChannelInterceptor fooInterceptor() { - return new ChannelInterceptor() { - @Override - public Message preSend(Message message, MessageChannel channel) { - return MessageBuilder.fromMessage(message).setHeader("intercepted", "true").build(); - } - }; - } - - @Bean - public MessageHandler sendMessage(StreamBridge streamBridge) { - return message -> { - streamBridge.send("outgoing-out-0", message); - }; - } - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/edgecases/GH1801Test.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/edgecases/GH1801Test.java deleted file mode 100644 index a02c1796b..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/function/edgecases/GH1801Test.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function.edgecases; - -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.Message; - -/** - * This test validates that the issue https://github.com/spring-cloud/spring-cloud-stream/issues/1801 - * is addressed. - * - * @author Oleg Zhurakousky - * - */ -public class GH1801Test { - - @Test - void test() { - SampleBootApplication.main("--spring.cloud.stream.defaultBinder=integration"); - } - - @SpringBootApplication - public static class SampleBootApplication { - public static void main(String... args) { - new SpringApplicationBuilder(SampleBootApplication.class).web(WebApplicationType.NONE).run(args); - } - } -} - -@Configuration -class StreamConfiguration { - - @Bean - public Consumer> consumer() { - return System.out::println; - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/kotlin/KotlinConfigurationTests.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/kotlin/KotlinConfigurationTests.java deleted file mode 100644 index 0e09608b4..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/kotlin/KotlinConfigurationTests.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.kotlin; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.binder.test.OutputDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.messaging.Message; - -import static org.assertj.core.api.Assertions.assertThat; - -public class KotlinConfigurationTests { - - @Test - void testKotlinSupplierPollableBean() { - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(KotlinTestConfiguration.class)) - .web(WebApplicationType.NONE) - .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=produceNames")) { - - OutputDestination output = context.getBean(OutputDestination.class); - Message result = output.receive(1000, "produceNames-out-0"); - assertThat(result.getPayload()).isEqualTo("Ricky".getBytes()); - result = output.receive(1000, "produceNames-out-0"); - assertThat(result.getPayload()).isEqualTo("Julien".getBytes()); - result = output.receive(1000, "produceNames-out-0"); - assertThat(result.getPayload()).isEqualTo("Bubbles".getBytes()); - } - } -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/utils/IntegrationTestsMockBinderConfiguration.java b/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/utils/IntegrationTestsMockBinderConfiguration.java deleted file mode 100644 index abeca7965..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/java/org/springframework/cloud/stream/utils/IntegrationTestsMockBinderConfiguration.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.utils; - -import org.mockito.Mockito; - -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Marius Bogoevici - */ -@Configuration -public class IntegrationTestsMockBinderConfiguration { - - @Bean - public Binder binder() { - return Mockito.mock(Binder.class, - Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS)); - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/kotlin/org/springframework/cloud/stream/kotlin/KotlinTestConfiguration.kt b/core/spring-cloud-stream-integration-tests/src/test/kotlin/org/springframework/cloud/stream/kotlin/KotlinTestConfiguration.kt deleted file mode 100644 index 350a1278b..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/kotlin/org/springframework/cloud/stream/kotlin/KotlinTestConfiguration.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.kotlin - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.cloud.function.context.PollableBean -import org.springframework.context.annotation.Configuration -import reactor.core.publisher.Flux - -@Configuration -@EnableAutoConfiguration -open class KotlinTestConfiguration { - - @PollableBean // it doesn't work with Kotlin lambda - open fun produceNames(): () -> Flux = { - Flux.just( - "Ricky", - "Julien", - "Bubbles" - ) - } - -} diff --git a/core/spring-cloud-stream-integration-tests/src/test/resources/binder1/META-INF/spring.binders b/core/spring-cloud-stream-integration-tests/src/test/resources/binder1/META-INF/spring.binders deleted file mode 100644 index b6ac12c76..000000000 --- a/core/spring-cloud-stream-integration-tests/src/test/resources/binder1/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -integration1:\ -org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration diff --git a/core/spring-cloud-stream-test-binder/pom.xml b/core/spring-cloud-stream-test-binder/pom.xml deleted file mode 100644 index 43f6fa294..000000000 --- a/core/spring-cloud-stream-test-binder/pom.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-test-binder - jar - spring-cloud-stream-test-binder - Spring Cloud Stream Test Binder - - - org.springframework.cloud - spring-cloud-stream-core - 4.1.0-SNAPSHOT - - - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.boot - spring-boot-starter-test - - - - diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/AbstractDestination.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/AbstractDestination.java deleted file mode 100644 index 793d5ee40..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/AbstractDestination.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.test; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.integration.channel.AbstractSubscribableChannel; -import org.springframework.messaging.SubscribableChannel; - -/** - * @author Oleg Zhurakousky - * - */ -abstract class AbstractDestination { - - private final List channels = new ArrayList<>(); - - SubscribableChannel getChannel(int index) { - return this.channels.get(index); - } - - void setChannel(SubscribableChannel channel) { - this.channels.add((AbstractSubscribableChannel) channel); - this.afterChannelIsSet(this.channels.size() - 1, ((AbstractSubscribableChannel) channel).getBeanName()); - } - - void afterChannelIsSet(int channelIndex, String name) { - // noop - } - - SubscribableChannel getChannelByName(String name) { - name = name.endsWith(".destination") ? name : name + ".destination"; - for (AbstractSubscribableChannel subscribableChannel : channels) { - if (subscribableChannel.getBeanName().equals(name)) { - return subscribableChannel; - } - } - return null; - } -} diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/FunctionBindingTestUtils.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/FunctionBindingTestUtils.java deleted file mode 100644 index d6938fcf0..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/FunctionBindingTestUtils.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2019-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.test; - -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binding.BindableProxyFactory; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.messaging.MessageChannel; - -/** - * A utility class to assist with just-in-time bindings. - * It is intended for internal framework testing and is NOT for public use. Breaking changes are likely!!! - * - * @author Oleg Zhurakousky - * @author Byungjun You - * @since 3.0.2 - * - */ -public final class FunctionBindingTestUtils { - - private FunctionBindingTestUtils() { - - } - - @SuppressWarnings("rawtypes") - public static void bind(ConfigurableApplicationContext applicationContext, Object function) { - try { - Object targetFunction = function; - if (function instanceof FunctionRegistration functionRegistration) { - targetFunction = functionRegistration.getTarget(); - } - String functionName = targetFunction instanceof Function ? "function" : (targetFunction instanceof Consumer ? "consumer" : "supplier"); - - System.setProperty("spring.cloud.function.definition", functionName); - applicationContext.getBeanFactory().registerSingleton(functionName, function); - - Object actualFunction = ((FunctionInvocationWrapper) applicationContext - .getBean(FunctionCatalog.class).lookup(functionName)).getTarget(); - - InitializingBean functionBindingRegistrar = applicationContext.getBean("functionBindingRegistrar", InitializingBean.class); - functionBindingRegistrar.afterPropertiesSet(); - - BindableProxyFactory bindingProxy = applicationContext.getBean("&" + functionName + "_binding", BindableProxyFactory.class); - bindingProxy.afterPropertiesSet(); - - InitializingBean functionBinder = applicationContext.getBean("functionInitializer", InitializingBean.class); - functionBinder.afterPropertiesSet(); - - BindingServiceProperties bindingProperties = applicationContext.getBean(BindingServiceProperties.class); - String inputBindingName = functionName + "-in-0"; - String outputBindingName = functionName + "-out-0"; - Map bindings = bindingProperties.getBindings(); - BindingProperties inputProperties = bindings.get(inputBindingName); - BindingProperties outputProperties = bindings.get(outputBindingName); - ConsumerProperties consumerProperties = inputProperties.getConsumer(); - ProducerProperties producerProperties = outputProperties.getProducer(); - - TestChannelBinder binder = applicationContext.getBean(TestChannelBinder.class); - if (actualFunction instanceof Supplier || actualFunction instanceof Function) { - Binding bindProducer = binder.bindProducer(outputProperties.getDestination(), - applicationContext.getBean(outputBindingName, MessageChannel.class), - producerProperties == null ? new ProducerProperties() : producerProperties); - bindProducer.start(); - } - if (actualFunction instanceof Consumer || actualFunction instanceof Function) { - Binding bindConsumer = binder.bindConsumer(inputProperties.getDestination(), null, - applicationContext.getBean(inputBindingName, MessageChannel.class), - consumerProperties == null ? new ConsumerProperties() : consumerProperties); - bindConsumer.start(); - } - } - catch (Exception e) { - throw new IllegalStateException("Failed to bind function", e); - } - finally { - System.clearProperty("spring.cloud.function.definition"); - } - } -} diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/InputDestination.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/InputDestination.java deleted file mode 100644 index 27d562c7d..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/InputDestination.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2017-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.test; - -import org.springframework.messaging.Message; - -/** - * Implementation of binder endpoint that represents the source destination (e.g., - * destination from which messages will be received by Processor.INPUT).
- * You can interact with it by calling {@link #send(Message)} operation. - * - * @author Oleg Zhurakousky - * - */ -public class InputDestination extends AbstractDestination { - - /** - * Allows the {@link Message} to be sent to a Binder to be delegated to a default binding - * destination (e.g., "function-in-0" for cases where you only have a single function with the name 'function'). - * @param message message to send - */ - public void send(Message message) { - this.getChannel(0).send(message); - } - - /** - * @param message message to send - * @param inputIndex input index - * @deprecated since 3.0.2 in favor of {@link #receive(long, String)} where you should use the actual binding name (e.g., "foo-in-0") - */ - @Deprecated - public void send(Message message, int inputIndex) { - this.getChannel(inputIndex).send(message); - } - - /** - * Allows the {@link Message} to be sent to a Binder's destination.
- * This needs a bit of clarification. Just like with any binder, 'destination' - * name and 'binding' name are usually the same unless additional configuration - * is provided. For example; Assume you have a function 'uppercase'. The - * 'binding' names for this function would be 'uppercase-in-0' (for input) and - * 'uppercase-out-0' (for output). The 'destination' names would match as well - * unless you decide to provide something like - * 'spring.cloud.stream.bindings.uppercase-in-0.destination=upper' at which - * point the binding names and destination names are different.
- *
- * So, it is important to remember that since this binder's goal is to emulate - * real binders and real messaging systems you are sending TO and receiving FROM - * destination (as if it was real broker destination) and binder will map and - * delegate what you send to the binder's destination to individual bindings as - * would the real binder. - * - * * - * - *
-	 *
-	 * // assume the following properties
-	 * "--spring.cloud.function.definition=uppercase",
-	 * "--spring.cloud.stream.bindings.uppercase-in-0.destination=upper",
-	 * "--spring.cloud.stream.bindings.uppercase-out-0.destination=upperout"
-	 *
-	 * // send/receive
-	 * inputDestination.send(message, "upper");
-	 * Message<byte[]> resultMessage = outputDestination.receive(1000, "upperout");
-	 *
-	 * // if 'destination' property is not provided for both input and output
-	 * inputDestination.send(message, "uppercase-in-0");
-	 * Message<byte[]> resultMessage = outputDestination.receive(1000, "uppercase-out-0");
-	 * 
- * - * @param message message to send - * @param destinationName the name of the destination - */ - public void send(Message message, String destinationName) { - this.getChannelByName(destinationName).send(message); - } - -} diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/OutputDestination.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/OutputDestination.java deleted file mode 100644 index 9d45a55a0..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/OutputDestination.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2017-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.test; - -import java.util.ArrayList; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.integration.channel.AbstractSubscribableChannel; -import org.springframework.messaging.Message; -import org.springframework.util.StringUtils; - -/** - * Implementation of binder endpoint that represents the target destination (e.g., - * destination which receives messages sent to Processor.OUTPUT)
- * You can interact with it by calling {@link #receive()} operation. - * - * @author Oleg Zhurakousky - * - */ -public class OutputDestination extends AbstractDestination { - - private final Log log = LogFactory.getLog(OutputDestination.class); - - private final ConcurrentHashMap>> messageQueues = new ConcurrentHashMap<>(); - - public Message receive(long timeout, String bindingName) { - try { - bindingName = bindingName.endsWith(".destination") ? bindingName : bindingName + ".destination"; - return this.outputQueue(bindingName).poll(timeout, TimeUnit.MILLISECONDS); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - return null; - } - - /** - * Will clear all output destinations. - * - * @since 3.0.6 - */ - public void clear() { - this.messageQueues.values().forEach(v -> v.clear()); - } - - /** - * Will clear output destination with specified name. - * - * @param destinationName the name of the output destination to be cleared. - * @return true if attempt to clear specific destination is successful otherwise false. - * @since 3.0.6 - */ - public boolean clear(String destinationName) { - String queueName = destinationName.endsWith(".destination") ? destinationName : destinationName + ".destination"; - if (StringUtils.hasText(destinationName) && this.messageQueues.containsKey(queueName)) { - this.messageQueues.get(queueName).clear(); - return true; - } - return false; - } - - /** - * Allows to access {@link Message}s received by this {@link OutputDestination}. - * This is a convenience method for cases when you only have one binding. - * For all other cases use {@link #receive(long, String)} method. - * - * @return received message - */ - public Message receive() { - return this.receive(0, 0); - } - - /** - * Allows to access {@link Message}s received by this {@link OutputDestination}. - * This is a convenience method for cases when you only have one binding. - * For all other cases use {@link #receive(long, String)} method. - * - * @param timeout timeout for receiving message - * @return received message - */ - public Message receive(long timeout) { - return this.receive(timeout, 0); - } - - @SuppressWarnings("unchecked") - @Override - void afterChannelIsSet(int channelIndex, String bindingName) { - if (((AbstractSubscribableChannel) this.getChannelByName(bindingName)).getSubscriberCount() < 1) { - this.getChannelByName(bindingName).subscribe(message -> this.outputQueue(bindingName).offer((Message) message)); - } - } - - private BlockingQueue> outputQueue(String bindingName) { - this.messageQueues.putIfAbsent(bindingName, new LinkedTransferQueue<>()); - return this.messageQueues.get(bindingName); - } - - private Message receive(long timeout, int bindingIndex) { - log.warn("!!!While 'receive(long timeout, int bindingIndex)' method may still work it is deprecated no longer supported. " - + "It will be removed after 3.1.3 release. Please use 'receive(long timeout, String bindingName)'"); - try { - BlockingQueue> destinationQueue = (new ArrayList<>(this.messageQueues.values())).get(bindingIndex); - return destinationQueue.poll(timeout, TimeUnit.MILLISECONDS); - } - catch (Exception e) { - Thread.currentThread().interrupt(); - } - return null; - } -} diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinder.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinder.java deleted file mode 100644 index 6d87e0225..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinder.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.test; - -import java.util.function.Consumer; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.test.TestChannelBinderProvisioner.SpringIntegrationConsumerDestination; -import org.springframework.cloud.stream.binder.test.TestChannelBinderProvisioner.SpringIntegrationProducerDestination; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.core.AttributeAccessor; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.acks.AcknowledgmentCallback; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.core.MessageSource; -import org.springframework.integration.endpoint.MessageProducerSupport; -import org.springframework.integration.handler.BridgeHandler; -import org.springframework.integration.support.DefaultErrorMessageStrategy; -import org.springframework.integration.support.ErrorMessageStrategy; -import org.springframework.integration.support.MapBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.retry.RecoveryCallback; -import org.springframework.retry.RetryCallback; -import org.springframework.retry.RetryContext; -import org.springframework.retry.RetryListener; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Implementation of {@link Binder} backed by Spring Integration framework. It is useful - * for localized demos and testing. - *

- * This binder extends from the same base class ({@link AbstractMessageChannelBinder}) as - * other binders (i.e., Rabbit, Kafka etc). Interaction with this binder is done via - * source and target destination which emulate real binder's destinations (i.e., Kafka - * topic)
- * The destination classes are - *

    - *
  • {@link InputDestination}
  • - *
  • {@link OutputDestination}
  • - *
- * Simply autowire them in your your application and send/receive messages. - *

- * You must also add {@link TestChannelBinderConfiguration} to your configuration. Below - * is the example using Spring Boot test.
- *
- * @RunWith(SpringJUnit4ClassRunner.class)
- * @SpringBootTest(classes = {SpringIntegrationBinderConfiguration.class, TestWithSIBinder.MyProcessor.class})
- * public class TestWithSIBinder {
- *     @Autowired
- *     private SourceDestination sourceDestination;
- *
- *     @Autowired
- *     private TargetDestination targetDestination;
- *
- *     @Test
- *     public void testWiring() {
- *         sourceDestination.send(new GenericMessage<String>("Hello"));
- *         assertEquals("Hello world",
- *             new String((byte[])targetDestination.receive().getPayload(), StandardCharsets.UTF_8));
- *     }
- *
- *     @SpringBootApplication
- *     @EnableBinding(Processor.class)
- *     public static class MyProcessor {
- *         @StreamListener(Processor.INPUT)
- *         @SendTo(Processor.OUTPUT)
- *         public String transform(String in) {
- *             return in + " world";
- *         }
- *     }
- * }
- * 
- * - * @author Oleg Zhurakousky - * @author Gary Russell - * - */ -public class TestChannelBinder extends - AbstractMessageChannelBinder { - - @Autowired - private BeanFactory beanFactory; - - private Message lastError; - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private MessageSource messageSourceDelegate = () -> new GenericMessage<>( - "polled data", new MapBuilder() - .put(MessageHeaders.CONTENT_TYPE, "text/plain") - .put(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, (AcknowledgmentCallback) status -> { - }).get()); - - public TestChannelBinder(TestChannelBinderProvisioner provisioningProvider) { - super(new String[] {}, provisioningProvider); - } - - /** - * Set a delegate {@link MessageSource} for pollable consumers. - * @param messageSourceDelegate the delegate. - */ - @Autowired(required = false) - public void setMessageSourceDelegate(MessageSource messageSourceDelegate) { - this.messageSourceDelegate = messageSourceDelegate; - } - - public Message getLastError() { - return this.lastError; - } - - public void resetLastError() { - this.lastError = null; - } - - @Override - protected MessageHandler createProducerMessageHandler(ProducerDestination destination, - ProducerProperties producerProperties, MessageChannel errorChannel) - throws Exception { - BridgeHandler handler = new BridgeHandler(); - handler.setBeanFactory(this.beanFactory); - handler.setOutputChannel( - ((SpringIntegrationProducerDestination) destination).getChannel()); - return handler; - } - - @Override - protected MessageProducer createConsumerEndpoint(ConsumerDestination destination, - String group, ConsumerProperties properties) throws Exception { - ErrorMessageStrategy errorMessageStrategy = new DefaultErrorMessageStrategy(); - SubscribableChannel siBinderInputChannel = ((SpringIntegrationConsumerDestination) destination) - .getChannel(); - - IntegrationMessageListeningContainer messageListenerContainer = new IntegrationMessageListeningContainer(); - IntegrationBinderInboundChannelAdapter adapter = new IntegrationBinderInboundChannelAdapter( - messageListenerContainer); - - String groupName = StringUtils.hasText(group) ? group : "anonymous"; - ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(destination, - groupName, properties); - if (properties.getMaxAttempts() > 1) { - adapter.setRetryTemplate(buildRetryTemplate(properties)); - adapter.setRecoveryCallback(errorInfrastructure.getRecoverer()); - } - else { - adapter.setErrorMessageStrategy(errorMessageStrategy); - adapter.setErrorChannel(errorInfrastructure.getErrorChannel()); - } - - siBinderInputChannel.subscribe(messageListenerContainer); - - return adapter; - } - - @Override - protected PolledConsumerResources createPolledConsumerResources(String name, - String group, ConsumerDestination destination, - ConsumerProperties consumerProperties) { - return new PolledConsumerResources(this.messageSourceDelegate, - registerErrorInfrastructure(destination, group, consumerProperties)); - } - - @Override - protected MessageHandler getErrorMessageHandler(ConsumerDestination destination, - String group, ConsumerProperties consumerProperties) { - return m -> { - this.logger.info("Error handled: " + m); - this.lastError = m; - }; - } - - /** - * Implementation of simple message listener container modeled after AMQP - * SimpleMessageListenerContainer. - */ - private static class IntegrationMessageListeningContainer implements MessageHandler { - - private Consumer> listener; - - @Override - public void handleMessage(Message message) throws MessagingException { - this.listener.accept(message); - } - - public void setMessageListener(Consumer> listener) { - this.listener = listener; - } - - } - - /** - * Implementation of inbound channel adapter modeled after AmqpInboundChannelAdapter. - */ - private static class IntegrationBinderInboundChannelAdapter - extends MessageProducerSupport { - - private static final ThreadLocal attributesHolder = new ThreadLocal(); - - private final IntegrationMessageListeningContainer listenerContainer; - - private RetryTemplate retryTemplate; - - private RecoveryCallback recoveryCallback; - - IntegrationBinderInboundChannelAdapter( - IntegrationMessageListeningContainer listenerContainer) { - this.listenerContainer = listenerContainer; - } - - // Temporarily unused until DLQ strategy for this binder becomes a requirement - public void setRecoveryCallback( - RecoveryCallback recoveryCallback) { - this.recoveryCallback = recoveryCallback; - } - - public void setRetryTemplate(RetryTemplate retryTemplate) { - this.retryTemplate = retryTemplate; - } - - @Override - protected void onInit() { - if (this.retryTemplate != null) { - Assert.state(getErrorChannel() == null, - "Cannot have an 'errorChannel' property when a 'RetryTemplate' is " - + "provided; use an 'ErrorMessageSendingRecoverer' in the 'recoveryCallback' property to " - + "send an error message when retries are exhausted"); - } - Listener messageListener = new Listener(); - if (this.retryTemplate != null) { - this.retryTemplate.registerListener(messageListener); - } - this.listenerContainer.setMessageListener(messageListener); - } - - protected class Listener implements RetryListener, Consumer> { - - @Override - @SuppressWarnings("unchecked") - public void accept(Message message) { - try { - if (IntegrationBinderInboundChannelAdapter.this.retryTemplate == null) { - try { - processMessage(message); - } - finally { - attributesHolder.remove(); - } - } - else { - IntegrationBinderInboundChannelAdapter.this.retryTemplate - .execute(context -> { - processMessage(message); - return null; - }, (RecoveryCallback) IntegrationBinderInboundChannelAdapter.this.recoveryCallback); - } - } - catch (RuntimeException e) { - if (getErrorChannel() != null) { - getMessagingTemplate() - .send(getErrorChannel(), - buildErrorMessage(null, new IllegalStateException( - "Message conversion failed: " + message, - e))); - } - else { - throw e; - } - } - } - - private void processMessage(Message message) { - sendMessage(message); - } - - @Override - public boolean open(RetryContext context, - RetryCallback callback) { - if (IntegrationBinderInboundChannelAdapter.this.recoveryCallback != null) { - attributesHolder.set(context); - } - return true; - } - - @Override - public void close(RetryContext context, - RetryCallback callback, Throwable throwable) { - attributesHolder.remove(); - } - - @Override - public void onError(RetryContext context, - RetryCallback callback, Throwable throwable) { - // Empty - } - - } - - } - -} diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinderConfiguration.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinderConfiguration.java deleted file mode 100644 index a62e7a123..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinderConfiguration.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.integration.config.EnableIntegration; - -/** - * {@link Binder} configuration backed by Spring Integration. - * - * Please see {@link TestChannelBinder} for more details. - * - * @param binding type - * @author Oleg Zhurakousky - * @author David Turanski - * @see TestChannelBinder - */ -@Configuration -@ConditionalOnMissingBean(Binder.class) -@Import(BinderFactoryAutoConfiguration.class) -@EnableIntegration -public class TestChannelBinderConfiguration { - - /** - * The name of the test binder. - */ - public static final String NAME = "integration"; - - /** - * Utility operation to return an array of configuration classes where function - * definitions are provided. Typically used for tests that do not rely on - * creating an SCSt boot application, yet require full {@link Binder} configuration. - * - * @param additionalConfigurationClasses config classes to be added to the default config - * @return an array of configuration classes - */ - public static Class[] getCompleteConfiguration( - Class... additionalConfigurationClasses) { - List> configClasses = new ArrayList<>(); - configClasses.add(TestChannelBinderConfiguration.class); - configClasses.add(BindingServiceConfiguration.class); - if (additionalConfigurationClasses != null) { - configClasses.addAll(Arrays.asList(additionalConfigurationClasses)); - } - return configClasses.toArray(new Class[] {}); - } - - /** - * Create an {@link ApplicationContextRunner} with user configuration using {@link #getCompleteConfiguration}. - * @param additionalConfigurationClasses config classes to be added to the default - * config - * @return the ApplicationContextRunner - */ - public static ApplicationContextRunner applicationContextRunner(Class... additionalConfigurationClasses) { - return new ApplicationContextRunner() - .withUserConfiguration(getCompleteConfiguration(additionalConfigurationClasses)); - } - - @Bean - public InputDestination sourceDestination() { - return new InputDestination(); - } - - @Bean - public OutputDestination targetDestination() { - return new OutputDestination(); - } - - @SuppressWarnings("unchecked") - @Bean - public Binder springIntegrationChannelBinder( - TestChannelBinderProvisioner provisioner) { - return (Binder) new TestChannelBinder( - provisioner); - } - - @Bean - public TestChannelBinderProvisioner springIntegrationProvisioner() { - return new TestChannelBinderProvisioner(); - } - -} diff --git a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinderProvisioner.java b/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinderProvisioner.java deleted file mode 100644 index a4a2cc37f..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/java/org/springframework/cloud/stream/binder/test/TestChannelBinderProvisioner.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder.test; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.cloud.stream.provisioning.ProvisioningException; -import org.springframework.cloud.stream.provisioning.ProvisioningProvider; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.PublishSubscribeChannel; -import org.springframework.messaging.Message; -import org.springframework.messaging.SubscribableChannel; - -/** - * {@link ProvisioningProvider} to support {@link TestChannelBinder}. It exists primarily - * to support {@link AbstractMessageChannel} semantics for creating - * {@link ConsumerDestination} and {@link ProducerDestination}, to interact with this - * {@link Binder}. - * - * @author Oleg Zhurakousky - * - */ -public class TestChannelBinderProvisioner - implements ProvisioningProvider { - - private final Map provisionedDestinations = new HashMap<>(); - - @Autowired - private InputDestination source; - - @Autowired - private OutputDestination target; - - /** - * Will provision producer destination as an SI {@link PublishSubscribeChannel}.
- * This provides convenience of registering additional subscriber (handler in the test - * method) along side of being able to call {@link OutputDestination#receive()} to get - * a {@link Message} for additional assertions. - */ - @Override - public ProducerDestination provisionProducerDestination(String name, - ProducerProperties properties) throws ProvisioningException { - SubscribableChannel destination = this.provisionDestination(name, true); - this.target.setChannel(destination); - return new SpringIntegrationProducerDestination(name, destination); - } - - /** - * Will provision consumer destination as SI {@link DirectChannel}. - */ - @Override - public ConsumerDestination provisionConsumerDestination(String name, String group, - ConsumerProperties properties) throws ProvisioningException { - SubscribableChannel destination = this.provisionDestination(name, false); - if (this.source != null) { - this.source.setChannel(destination); - } - return new SpringIntegrationConsumerDestination(name, destination); - } - - private SubscribableChannel provisionDestination(String name, boolean pubSub) { - String destinationName = name + ".destination"; - SubscribableChannel destination = this.provisionedDestinations - .get(destinationName); - if (destination == null) { - destination = new PublishSubscribeChannel(); - ((AbstractMessageChannel) destination).setBeanName(destinationName); - ((AbstractMessageChannel) destination).setComponentName(destinationName); - this.provisionedDestinations.put(destinationName, destination); - } - return destination; - } - - class SpringIntegrationConsumerDestination implements ConsumerDestination { - - private final String name; - - private final SubscribableChannel channel; - - SpringIntegrationConsumerDestination(String name, SubscribableChannel channel) { - this.name = name; - this.channel = channel; - } - - public SubscribableChannel getChannel() { - return this.channel; - } - - @Override - public String getName() { - return this.name; - } - - } - - class SpringIntegrationProducerDestination implements ProducerDestination { - - private final String name; - - private final SubscribableChannel channel; - - SpringIntegrationProducerDestination(String name, SubscribableChannel channel) { - this.name = name; - this.channel = channel; - } - - @Override - public String getNameForPartition(int partition) { - return this.getName() + partition; - } - - public SubscribableChannel getChannel() { - return this.channel; - } - - @Override - public String getName() { - return this.name; - } - - } - -} diff --git a/core/spring-cloud-stream-test-binder/src/main/resources/META-INF/spring.binders b/core/spring-cloud-stream-test-binder/src/main/resources/META-INF/spring.binders deleted file mode 100644 index 9fd72cf81..000000000 --- a/core/spring-cloud-stream-test-binder/src/main/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -integration:\ -org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration diff --git a/core/spring-cloud-stream-test-binder/src/test/java/org/springframework/cloud/stream/binder/test/TestChannelBinderTests.java b/core/spring-cloud-stream-test-binder/src/test/java/org/springframework/cloud/stream/binder/test/TestChannelBinderTests.java deleted file mode 100644 index 1c6ea0133..000000000 --- a/core/spring-cloud-stream-test-binder/src/test/java/org/springframework/cloud/stream/binder/test/TestChannelBinderTests.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.test; - -import java.lang.reflect.Method; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.util.ReflectionUtils; - -/** - * - * @author Oleg Zhurakousky - * - */ -public class TestChannelBinderTests { - - @Test - public void test() throws Exception { - // nothing to assert. no failure on this test signifies success - try (ConfigurableApplicationContext context = new SpringApplicationBuilder( - TestChannelBinderConfiguration.getCompleteConfiguration(SampleConfiguration.class)) - .web(WebApplicationType.NONE).run("--spring.jmx.enabled=false", - "--spring.cloud.stream.bindings.function-in-0.destination=input")) { - TestChannelBinder binder = context.getBean(TestChannelBinder.class); - Method registerErrorInfrastructure = ReflectionUtils - .findMethod(TestChannelBinder.class, "registerErrorInfrastructure", ProducerDestination.class, String.class); - registerErrorInfrastructure.setAccessible(true); - ProducerDestination destination = new ProducerDestination() { - @Override - public String getNameForPartition(int partition) { - return "sample"; - } - - @Override - public String getName() { - return "sample"; - } - }; - registerErrorInfrastructure.invoke(binder, destination, "function-in-0"); - destination = new ProducerDestination() { - @Override - public String getNameForPartition(int partition) { - return "sample"; - } - - @Override - public String getName() { - return "sample"; - } - }; - registerErrorInfrastructure.invoke(binder, destination, "function-in-0"); - } - } - - @EnableAutoConfiguration - public static class SampleConfiguration { - - } -} diff --git a/core/spring-cloud-stream-test-support/.jdk8 b/core/spring-cloud-stream-test-support/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/spring-cloud-stream-test-support/pom.xml b/core/spring-cloud-stream-test-support/pom.xml deleted file mode 100644 index b9b048c3e..000000000 --- a/core/spring-cloud-stream-test-support/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream-test-support - jar - - spring-cloud-stream-test-support - Test support for binder implementations - - - org.springframework.cloud - spring-cloud-stream-core - 4.1.0-SNAPSHOT - - - - - org.springframework.integration - spring-integration-test - - - org.apache.avro - avro-compiler - - - - - com.fasterxml.jackson.core - jackson-databind - - - org.springframework - spring-web - - - org.springframework.cloud - spring-cloud-stream - - - org.springframework.boot - spring-boot-starter-test - compile - - - diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java deleted file mode 100644 index df82eb3cb..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright 2013-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -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.junit.jupiter.api.TestInfo; - -import org.springframework.cloud.stream.binding.MessageConverterConfigurer; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory; -import org.springframework.context.Lifecycle; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.PollableChannel; -import org.springframework.messaging.converter.SmartMessageConverter; -import org.springframework.util.Assert; -import org.springframework.util.MimeTypeUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @author David Turanski - * @author Mark Fisher - * @author Marius Bogoevici - * @author Oleg Zhurakousky - * @author Jacob Severson - * @author Artem Bilan - */ -// @checkstyle:off -@SuppressWarnings("unchecked") -public abstract class AbstractBinderTests, CP, PP>, CP extends ConsumerProperties, PP extends ProducerProperties> { - - // @checkstyle:on - - protected final Log logger = LogFactory.getLog(this.getClass()); - - protected B testBinder; - - protected SmartMessageConverter messageConverter; - - protected GenericApplicationContext applicationContext; - - /** - * Subclasses may override this default value to have tests wait longer for a message - * receive, for example if running in an environment that is known to be slow (e.g. - * travis). - */ - protected double timeoutMultiplier = 1.0D; - - @BeforeEach - public void before() { - applicationContext = new GenericApplicationContext(); - applicationContext.refresh(); - this.messageConverter = new CompositeMessageConverterFactory() - .getMessageConverterForAllRegistered(); - } - - /** - * Attempt to receive a message on the given channel, waiting up to 1s (times the - * {@link #timeoutMultiplier}). - */ - protected Message receive(PollableChannel channel) { - return receive(channel, 1); - } - - /** - * Attempt to receive a message on the given channel, waiting up to 1s * - * additionalMultiplier * {@link #timeoutMultiplier}). - * - * Allows accomodating tests which are slower than normal (e.g. retry). - */ - protected Message receive(PollableChannel channel, int additionalMultiplier) { - long startTime = System.currentTimeMillis(); - Message receive = channel - .receive((int) (1000 * this.timeoutMultiplier * additionalMultiplier)); - long elapsed = System.currentTimeMillis() - startTime; - this.logger.debug("receive() took " + elapsed / 1000 + " seconds"); - return receive; - } - - @Test - @SuppressWarnings("rawtypes") - public void testClean(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - Binding foo0ProducerBinding = binder.bindProducer( - String.format("foo%s0", getDestinationNameDelimiter()), - this.createBindableChannel("output", new BindingProperties()), - createProducerProperties(testInfo)); - Binding foo0ConsumerBinding = binder.bindConsumer( - String.format("foo%s0", getDestinationNameDelimiter()), "testClean", - this.createBindableChannel("input", new BindingProperties()), - createConsumerProperties()); - Binding foo1ProducerBinding = binder.bindProducer( - String.format("foo%s1", getDestinationNameDelimiter()), - this.createBindableChannel("output", new BindingProperties()), - createProducerProperties(testInfo)); - Binding foo1ConsumerBinding = binder.bindConsumer( - String.format("foo%s1", getDestinationNameDelimiter()), "testClean", - this.createBindableChannel("input", new BindingProperties()), - createConsumerProperties()); - Binding foo2ProducerBinding = binder.bindProducer( - String.format("foo%s2", getDestinationNameDelimiter()), - this.createBindableChannel("output", new BindingProperties()), - createProducerProperties(testInfo)); - foo0ProducerBinding.unbind(); - assertThat(TestUtils - .getPropertyValue(foo0ProducerBinding, "lifecycle", Lifecycle.class) - .isRunning()).isFalse(); - foo0ConsumerBinding.unbind(); - foo1ProducerBinding.unbind(); - assertThat(TestUtils - .getPropertyValue(foo0ConsumerBinding, "lifecycle", Lifecycle.class) - .isRunning()).isFalse(); - assertThat(TestUtils - .getPropertyValue(foo1ProducerBinding, "lifecycle", Lifecycle.class) - .isRunning()).isFalse(); - foo1ConsumerBinding.unbind(); - foo2ProducerBinding.unbind(); - assertThat(TestUtils - .getPropertyValue(foo1ConsumerBinding, "lifecycle", Lifecycle.class) - .isRunning()).isFalse(); - assertThat(TestUtils - .getPropertyValue(foo2ProducerBinding, "lifecycle", Lifecycle.class) - .isRunning()).isFalse(); - } - - @SuppressWarnings("rawtypes") - @Test - public void testSendAndReceive(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - BindingProperties outputBindingProperties = createProducerBindingProperties( - createProducerProperties(testInfo)); - DirectChannel moduleOutputChannel = createBindableChannel("output", - outputBindingProperties); - - BindingProperties inputBindingProperties = createConsumerBindingProperties( - createConsumerProperties()); - DirectChannel moduleInputChannel = createBindableChannel("input", - inputBindingProperties); - - Binding producerBinding = binder.bindProducer( - String.format("foo%s0", getDestinationNameDelimiter()), - moduleOutputChannel, outputBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer( - String.format("foo%s0", getDestinationNameDelimiter()), - "testSendAndReceive", moduleInputChannel, - inputBindingProperties.getConsumer()); - Message message = MessageBuilder.withPayload("foo") - .setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build(); - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - - moduleOutputChannel.send(message); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - - assertThat(inboundMessageRef.get().getPayload()).isEqualTo("foo".getBytes()); - assertThat(inboundMessageRef.get().getHeaders().get(MessageHeaders.CONTENT_TYPE) - .toString()).isEqualTo("text/plain"); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - @SuppressWarnings("rawtypes") - public void testSendAndReceiveMultipleTopics(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties(testInfo)); - - DirectChannel moduleOutputChannel1 = createBindableChannel("output1", - producerBindingProperties); - - DirectChannel moduleOutputChannel2 = createBindableChannel("output2", - producerBindingProperties); - - QueueChannel moduleInputChannel = new QueueChannel(); - - Binding producerBinding1 = binder.bindProducer( - String.format("foo%sxy", getDestinationNameDelimiter()), - moduleOutputChannel1, producerBindingProperties.getProducer()); - Binding producerBinding2 = binder.bindProducer( - String.format("foo%syz", - - getDestinationNameDelimiter()), - moduleOutputChannel2, producerBindingProperties.getProducer()); - - Binding consumerBinding1 = binder.bindConsumer( - String.format("foo%sxy", getDestinationNameDelimiter()), - "testSendAndReceiveMultipleTopics", moduleInputChannel, - createConsumerProperties()); - Binding consumerBinding2 = binder.bindConsumer( - String.format("foo%syz", getDestinationNameDelimiter()), - "testSendAndReceiveMultipleTopics", moduleInputChannel, - createConsumerProperties()); - - String testPayload1 = "foo" + UUID.randomUUID().toString(); - Message message1 = MessageBuilder.withPayload(testPayload1.getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, - MimeTypeUtils.APPLICATION_OCTET_STREAM) - .build(); - String testPayload2 = "foo" + UUID.randomUUID().toString(); - Message message2 = MessageBuilder.withPayload(testPayload2.getBytes()) - .setHeader(MessageHeaders.CONTENT_TYPE, - MimeTypeUtils.APPLICATION_OCTET_STREAM) - .build(); - - // Let the consumer actually bind to the producer before sending a msg - binderBindUnbindLatency(); - moduleOutputChannel1.send(message1); - moduleOutputChannel2.send(message2); - - Message[] messages = new Message[2]; - messages[0] = receive(moduleInputChannel); - messages[1] = receive(moduleInputChannel); - - assertThat(messages[0]).isNotNull(); - assertThat(messages[1]).isNotNull(); - assertThat(messages).extracting("payload").containsExactlyInAnyOrder( - testPayload1.getBytes(), testPayload2.getBytes()); - - producerBinding1.unbind(); - producerBinding2.unbind(); - - consumerBinding1.unbind(); - consumerBinding2.unbind(); - } - - @Test - @SuppressWarnings("rawtypes") - public void testSendAndReceiveNoOriginalContentType(TestInfo testInfo) throws Exception { - Binder binder = getBinder(); - - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties(testInfo)); - DirectChannel moduleOutputChannel = createBindableChannel("output", - producerBindingProperties); - BindingProperties inputBindingProperties = createConsumerBindingProperties( - createConsumerProperties()); - DirectChannel moduleInputChannel = createBindableChannel("input", - inputBindingProperties); - Binding producerBinding = binder.bindProducer( - String.format("bar%s0", getDestinationNameDelimiter()), - moduleOutputChannel, producerBindingProperties.getProducer()); - Binding consumerBinding = binder.bindConsumer( - String.format("bar%s0", getDestinationNameDelimiter()), - "testSendAndReceiveNoOriginalContentType", moduleInputChannel, - createConsumerProperties()); - binderBindUnbindLatency(); - - Message message = MessageBuilder.withPayload("foo") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build(); - moduleOutputChannel.send(message); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference> inboundMessageRef = new AtomicReference>(); - moduleInputChannel.subscribe(message1 -> { - try { - inboundMessageRef.set((Message) message1); - } - finally { - latch.countDown(); - } - }); - - moduleOutputChannel.send(message); - Assert.isTrue(latch.await(5, TimeUnit.SECONDS), "Failed to receive message"); - assertThat(inboundMessageRef.get()).isNotNull(); - assertThat(inboundMessageRef.get().getPayload()).isEqualTo("foo".getBytes()); - assertThat(inboundMessageRef.get().getHeaders().get(MessageHeaders.CONTENT_TYPE) - .toString()).isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE); - producerBinding.unbind(); - consumerBinding.unbind(); - } - - protected abstract B getBinder() throws Exception; - - protected abstract CP createConsumerProperties(); - - protected abstract PP createProducerProperties(TestInfo testInfo); - - protected final BindingProperties createConsumerBindingProperties( - CP consumerProperties) { - BindingProperties bindingProperties = new BindingProperties(); - bindingProperties.setConsumer(consumerProperties); - return bindingProperties; - } - - protected BindingProperties createProducerBindingProperties(PP producerProperties) { - BindingProperties bindingProperties = new BindingProperties(); - bindingProperties.setProducer(producerProperties); - return bindingProperties; - } - - protected DirectChannel createBindableChannel(String channelName, - BindingProperties bindingProperties) throws Exception { - // The 'channelName.contains("input")' is strictly for convenience to avoid - // modifications in multiple tests - return this.createBindableChannel(channelName, bindingProperties, - channelName.contains("input")); - } - - protected DirectChannel createBindableChannel(String channelName, - BindingProperties bindingProperties, boolean inputChannel) throws Exception { - MessageConverterConfigurer messageConverterConfigurer = createConverterConfigurer( - channelName, bindingProperties); - DirectChannel channel = new DirectChannel(); - channel.setBeanName(channelName); - if (inputChannel) { - messageConverterConfigurer.configureInputChannel(channel, channelName); - } - else { - messageConverterConfigurer.configureOutputChannel(channel, channelName); - } - return channel; - } - - protected DefaultPollableMessageSource createBindableMessageSource(String bindingName, - BindingProperties bindingProperties) throws Exception { - DefaultPollableMessageSource source = new DefaultPollableMessageSource( - new CompositeMessageConverterFactory() - .getMessageConverterForAllRegistered()); - createConverterConfigurer(bindingName, bindingProperties) - .configurePolledMessageSource(source, bindingName); - return source; - } - - private MessageConverterConfigurer createConverterConfigurer(String channelName, - BindingProperties bindingProperties) throws Exception { - BindingServiceProperties bindingServiceProperties = new BindingServiceProperties(); - bindingServiceProperties.getBindings().put(channelName, bindingProperties); - bindingServiceProperties.setApplicationContext(applicationContext); - bindingServiceProperties.setConversionService(new DefaultConversionService()); - bindingServiceProperties.afterPropertiesSet(); - MessageConverterConfigurer messageConverterConfigurer = new MessageConverterConfigurer( - bindingServiceProperties, - new CompositeMessageConverterFactory(null, null, null).getMessageConverterForAllRegistered()); - messageConverterConfigurer.setBeanFactory(applicationContext.getBeanFactory()); - return messageConverterConfigurer; - } - - @AfterEach - public void cleanup() { - if (this.testBinder != null) { - this.testBinder.cleanup(); - } - } - - /** - * If appropriate, let the binder middleware settle down a bit while binding/unbinding - * actually happens. - */ - protected void binderBindUnbindLatency() throws InterruptedException { - // default none - } - - /** - * Create a new spy on the given 'queue'. This allows de-correlating the creation of - * the 'connection' from its actual usage, which may be needed by some implementations - * to see messages sent after connection creation. - */ - public abstract Spy spyOn(String name); - - /** - * Set the delimiter that will be used in the message source/target name. Some brokers - * may have naming constraints (such as SQS), so this provides a way to override the - * character being used as a delimiter. The default is a period. - */ - protected String getDestinationNameDelimiter() { - return "."; - } - - @SuppressWarnings("unused") // it is used via reflection - private Station echoStation(Station station) { - return station; - } - - @SuppressWarnings("unused") // it is used via reflection - private String echoStationString(String station) { - return station; - } - - public static class Station { - - List readings = new ArrayList<>(); - - public List getReadings() { - return this.readings; - } - - public void setReadings(List readings) { - this.readings = readings; - } - - @SuppressWarnings("serial") - public static class Readings implements Serializable { - - public String stationid; - - public String customerid; - - public String timestamp; - - public String getStationid() { - return this.stationid; - } - - public void setStationid(String stationid) { - this.stationid = stationid; - } - - public String getCustomerid() { - return this.customerid; - } - - public void setCustomerid(String customerid) { - this.customerid = customerid; - } - - public String getTimestamp() { - return this.timestamp; - } - - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - } - - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractPollableConsumerTestBinder.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractPollableConsumerTestBinder.java deleted file mode 100644 index 956cfa048..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractPollableConsumerTestBinder.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; - -/** - * @param binder type - * @param consumer properties type - * @param producer properties type - * @author Gary Russell - * @since 2.0 - */ -// @checkstyle:off -public abstract class AbstractPollableConsumerTestBinder, CP extends ConsumerProperties, PP extends ProducerProperties> - extends AbstractTestBinder - implements PollableConsumerBinder { - - // @checkstyle:on - - private PollableConsumerBinder binder; - - @SuppressWarnings("unchecked") - public void setPollableConsumerBinder( - PollableConsumerBinder binder) { - super.setBinder((C) binder); - this.binder = binder; - } - - @Override - public Binding> bindPollableConsumer(String name, - String group, PollableSource inboundBindTarget, - CP consumerProperties) { - return this.binder.bindPollableConsumer(name, group, inboundBindTarget, - consumerProperties); - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java deleted file mode 100644 index cb5eff10e..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2014-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.HashSet; -import java.util.Set; - -import org.springframework.integration.channel.AbstractSubscribableChannel; -import org.springframework.messaging.MessageChannel; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -/** - * Abstract class that adds test support for {@link Binder}. - * - * @param binder type - * @param consumer properties type - * @param producer properties type - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Mark Fisher - * @author Oleg Zhurakousky - * @author Byungjun You - */ -// @checkstyle:off -public abstract class AbstractTestBinder, CP extends ConsumerProperties, PP extends ProducerProperties> - implements Binder { - - // @checkstyle:on - - protected Set queues = new HashSet(); - - private C binder; - - @Override - public Binding bindConsumer(String name, String group, - MessageChannel moduleInputChannel, CP properties) { - this.checkChannelIsConfigured(moduleInputChannel, properties); - this.queues.add(name); - return this.binder.bindConsumer(name, group, moduleInputChannel, properties); - } - - @Override - public Binding bindProducer(String name, - MessageChannel moduleOutputChannel, PP properties) { - this.queues.add(name); - return this.binder.bindProducer(name, moduleOutputChannel, properties); - } - - public C getCoreBinder() { - return this.binder; - } - - public abstract void cleanup(); - - public C getBinder() { - return this.binder; - } - - public void setBinder(C binder) { - try { - binder.afterPropertiesSet(); - } - catch (Exception e) { - throw new RuntimeException("Failed to initialize binder", e); - } - this.binder = binder; - } - - /* - * This will ensure that any MessageChannel that was passed to one of the bind*() - * methods was properly configured (i.e., interceptors, converters etc). see - * org.springframework.cloud.stream.binding.MessageConverterConfigurer - */ - private void checkChannelIsConfigured(MessageChannel messageChannel, CP properties) { - if (messageChannel instanceof AbstractSubscribableChannel subscribableMessageChannel - && !properties.isUseNativeDecoding()) { - Assert.isTrue( - !CollectionUtils - .isEmpty(subscribableMessageChannel.getInterceptors()), - "'messageChannel' appears to be misconfigured. " - + "Consider creating channel via AbstractBinderTest.createBindableChannel(..)"); - } - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/BinderTestEnvironmentPostProcessor.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/BinderTestEnvironmentPostProcessor.java deleted file mode 100644 index bcea76db1..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/BinderTestEnvironmentPostProcessor.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; - -/** - * An {@link EnvironmentPostProcessor} that sets some common configuration properties (log - * config etc.,) for binder tests. - * - * @author Ilayaperumal Gopinathan - */ -public class BinderTestEnvironmentPostProcessor implements EnvironmentPostProcessor { - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, - SpringApplication application) { - Map propertiesToAdd = new HashMap<>(); - propertiesToAdd.put("logging.pattern.console", - "%d{ISO8601} %5p %t %c{2}:%L - %m%n"); - environment.getPropertySources().addLast( - new MapPropertySource("binderTestPropertiesConfig", propertiesToAdd)); - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/BinderTestUtils.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/BinderTestUtils.java deleted file mode 100644 index 0387a0bd5..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/BinderTestUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2014-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.cloud.stream.binder; - -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.integration.support.MessageBuilderFactory; -import org.springframework.integration.support.MutableMessageBuilderFactory; -import org.springframework.integration.support.utils.IntegrationUtils; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Gary Russell - */ -public abstract class BinderTestUtils { - - /** - * Mocked application context. - */ - public static final AbstractApplicationContext MOCK_AC = mock( - AbstractApplicationContext.class); - - /** - * Mocked application bean factory. - */ - public static final ConfigurableListableBeanFactory MOCK_BF = mock( - ConfigurableListableBeanFactory.class); - - private static final MessageBuilderFactory mbf = new MutableMessageBuilderFactory(); - - static { - when(MOCK_BF.getBean( - IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME, - MessageBuilderFactory.class)).thenReturn(mbf); - when(MOCK_AC.getBean( - IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME, - MessageBuilderFactory.class)).thenReturn(mbf); - when(MOCK_AC.getBeanFactory()).thenReturn(MOCK_BF); - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java deleted file mode 100644 index d86954925..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2014-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import org.assertj.core.api.Condition; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.context.Lifecycle; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.util.MimeTypeUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for binders that support partitioning. - * - * @author Gary Russell - * @author Mark Fisher - * @author Marius Bogoevici - * @author Vinicius Carvalho - */ -// @checkstyle:off -public abstract class PartitionCapableBinderTests, CP, PP>, CP extends ConsumerProperties, PP extends ProducerProperties> - extends AbstractBinderTests { - - // @checkstyle:on - - protected static final SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); - - @Test - @SuppressWarnings("unchecked") - public void testAnonymousGroup(TestInfo testInfo) throws Exception { - B binder = getBinder(); - BindingProperties producerBindingProperties = createProducerBindingProperties( - createProducerProperties(testInfo)); - DirectChannel output = createBindableChannel("output", producerBindingProperties); - Binding producerBinding = binder.bindProducer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), output, - (PP) producerBindingProperties.getProducer()); - - QueueChannel input1 = new QueueChannel(); - Binding binding1 = binder.bindConsumer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, - input1, createConsumerProperties()); - - QueueChannel input2 = new QueueChannel(); - Binding binding2 = binder.bindConsumer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, - input2, createConsumerProperties()); - - String testPayload1 = "foo-" + UUID.randomUUID().toString(); - output.send(MessageBuilder.withPayload(testPayload1) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - - Message receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload1); - - Message receivedMessage2 = (Message) receive(input2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload1); - - binding2.unbind(); - - String testPayload2 = "foo-" + UUID.randomUUID().toString(); - output.send(MessageBuilder.withPayload(testPayload2) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - - binding2 = binder.bindConsumer( - String.format("defaultGroup%s0", getDestinationNameDelimiter()), null, - input2, createConsumerProperties()); - String testPayload3 = "foo-" + UUID.randomUUID().toString(); - output.send(MessageBuilder.withPayload(testPayload3) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isEqualTo(testPayload2); - receivedMessage1 = (Message) receive(input1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String(receivedMessage1.getPayload())).isNotNull(); - - receivedMessage2 = (Message) receive(input2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String(receivedMessage2.getPayload())).isEqualTo(testPayload3); - - producerBinding.unbind(); - binding1.unbind(); - binding2.unbind(); - } - - @Test - public void testOneRequiredGroup(TestInfo testInfo) throws Exception { - B binder = getBinder(); - PP producerProperties = createProducerProperties(testInfo); - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - String testDestination = "testDestination" - + UUID.randomUUID().toString().replace("-", ""); - - producerProperties.setRequiredGroups("test1"); - Binding producerBinding = binder.bindProducer(testDestination, - output, producerProperties); - - String testPayload = "foo-" + UUID.randomUUID().toString(); - output.send(MessageBuilder.withPayload(testPayload) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - - QueueChannel inbound1 = new QueueChannel(); - Binding consumerBinding = binder.bindConsumer(testDestination, - "test1", inbound1, createConsumerProperties()); - - Message receivedMessage1 = receive(inbound1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String((byte[]) receivedMessage1.getPayload())) - .isEqualTo(testPayload); - - producerBinding.unbind(); - consumerBinding.unbind(); - } - - @Test - public void testTwoRequiredGroups(TestInfo testInfo) throws Exception { - B binder = getBinder(); - PP producerProperties = createProducerProperties(testInfo); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - - String testDestination = "testDestination" - + UUID.randomUUID().toString().replace("-", ""); - - producerProperties.setRequiredGroups("test1", "test2"); - Binding producerBinding = binder.bindProducer(testDestination, - output, producerProperties); - - String testPayload = "foo-" + UUID.randomUUID().toString(); - output.send(MessageBuilder.withPayload(testPayload) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - - QueueChannel inbound1 = new QueueChannel(); - Binding consumerBinding1 = binder.bindConsumer(testDestination, - "test1", inbound1, createConsumerProperties()); - QueueChannel inbound2 = new QueueChannel(); - Binding consumerBinding2 = binder.bindConsumer(testDestination, - "test2", inbound2, createConsumerProperties()); - - Message receivedMessage1 = receive(inbound1); - assertThat(receivedMessage1).isNotNull(); - assertThat(new String((byte[]) receivedMessage1.getPayload())) - .isEqualTo(testPayload); - Message receivedMessage2 = receive(inbound2); - assertThat(receivedMessage2).isNotNull(); - assertThat(new String((byte[]) receivedMessage2.getPayload())) - .isEqualTo(testPayload); - - consumerBinding1.unbind(); - consumerBinding2.unbind(); - producerBinding.unbind(); - } - - @Test - public void testPartitionedModuleSpEL(TestInfo testInfo) throws Exception { - B binder = getBinder(); - - CP consumerProperties = createConsumerProperties(); - consumerProperties.setConcurrency(2); - consumerProperties.setInstanceIndex(0); - consumerProperties.setInstanceCount(3); - consumerProperties.setPartitioned(true); - QueueChannel input0 = new QueueChannel(); - input0.setBeanName("test.input0S"); - Binding input0Binding = binder.bindConsumer( - String.format("part%s0", getDestinationNameDelimiter()), - "testPartitionedModuleSpEL", input0, consumerProperties); - consumerProperties.setInstanceIndex(1); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1S"); - Binding input1Binding = binder.bindConsumer( - String.format("part%s0", getDestinationNameDelimiter()), - "testPartitionedModuleSpEL", input1, consumerProperties); - consumerProperties.setInstanceIndex(2); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2S"); - Binding input2Binding = binder.bindConsumer( - String.format("part%s0", getDestinationNameDelimiter()), - "testPartitionedModuleSpEL", input2, consumerProperties); - - PP producerProperties = createProducerProperties(testInfo); - producerProperties.setPartitionKeyExpression( - spelExpressionParser.parseExpression("payload")); - producerProperties.setPartitionSelectorExpression( - spelExpressionParser.parseExpression("hashCode()")); - producerProperties.setPartitionCount(3); - - DirectChannel output = createBindableChannel("output", - createProducerBindingProperties(producerProperties)); - output.setBeanName("test.output"); - Binding outputBinding = binder.bindProducer( - String.format("part%s0", getDestinationNameDelimiter()), output, - producerProperties); - try { - Object endpoint = extractEndpoint(outputBinding); - checkRkExpressionForPartitionedModuleSpEL(endpoint); - } - catch (UnsupportedOperationException ignored) { - } - - Message message2 = MessageBuilder.withPayload("2") - .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42) - .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 43).build(); - output.send(message2); - output.send(MessageBuilder.withPayload("1") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - output.send(MessageBuilder.withPayload("0") - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN) - .build()); - - Message receive0 = receive(input0); - assertThat(receive0).isNotNull(); - Message receive1 = receive(input1); - assertThat(receive1).isNotNull(); - Message receive2 = receive(input2); - assertThat(receive2).isNotNull(); - - Condition> correlationHeadersForPayload2 = new Condition>() { - @Override - public boolean matches(Message value) { - IntegrationMessageHeaderAccessor accessor = new IntegrationMessageHeaderAccessor( - value); - return "foo".equals(accessor.getCorrelationId()) - && 42 == accessor.getSequenceNumber() - && 43 == accessor.getSequenceSize(); - } - }; - - if (usesExplicitRouting()) { - assertThat(receive0.getPayload()).isEqualTo("0".getBytes()); - assertThat(receive1.getPayload()).isEqualTo("1".getBytes()); - assertThat(receive2.getPayload()).isEqualTo("2".getBytes()); - assertThat(receive2).has(correlationHeadersForPayload2); - } - else { - List> receivedMessages = Arrays.asList(receive0, receive1, - receive2); - assertThat(receivedMessages).extracting("payload").containsExactlyInAnyOrder( - "0".getBytes(), "1".getBytes(), "2".getBytes()); - Condition> payloadIs2 = new Condition>() { - - @Override - public boolean matches(Message value) { - return value.getPayload().equals("2".getBytes()); - } - }; - assertThat(receivedMessages).filteredOn(payloadIs2).areExactly(1, - correlationHeadersForPayload2); - - } - input0Binding.unbind(); - input1Binding.unbind(); - input2Binding.unbind(); - outputBinding.unbind(); - } - - protected void checkRkExpressionForPartitionedModuleSpEL(Object endpoint) { - assertThat(getEndpointRouting(endpoint)) - .contains(getExpectedRoutingBaseDestination( - String.format("part%s0", getDestinationNameDelimiter()), "test") - + "-' + headers['partition']"); - } - - /** - * Implementations should return whether the binder under test uses "explicit" routing - * (e.g. Rabbit) whereby Spring Cloud Stream is responsible for assigning a partition - * and knows which exact consumer will receive the message (i.e. honor - * "partitionIndex") or "implicit" routing (e.g. Kafka) whereby the only guarantee is - * that messages will be spread, but we don't control exactly which consumer gets - * which message. - */ - protected abstract boolean usesExplicitRouting(); - - /** - * For implementations that rely on explicit routing, return the routing expression. - */ - protected String getEndpointRouting(Object endpoint) { - throw new UnsupportedOperationException(); - } - - /** - * For implementations that rely on explicit routing, return the expected base - * destination (the part that precedes '-partition' within the expression). - */ - protected String getExpectedRoutingBaseDestination(String name, String group) { - throw new UnsupportedOperationException(); - } - - protected abstract String getClassUnderTestName(); - - protected Lifecycle extractEndpoint(Binding binding) { - DirectFieldAccessor accessor = new DirectFieldAccessor(binding); - return (Lifecycle) accessor.getPropertyValue("lifecycle"); - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/PartitionTestSupport.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/PartitionTestSupport.java deleted file mode 100644 index da73364f7..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/PartitionTestSupport.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014-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.cloud.stream.binder; - -import org.springframework.messaging.Message; - -/** - * @author Gary Russell - */ -public class PartitionTestSupport - implements PartitionKeyExtractorStrategy, PartitionSelectorStrategy { - - @Override - public int selectPartition(Object key, int divisor) { - return key.hashCode() % divisor; - } - - @Override - public Object extractKey(Message message) { - return message.getPayload(); - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/Spy.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/Spy.java deleted file mode 100644 index ee0ef97c0..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/Spy.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -/** - * Represents an out-of-band connection to the underlying middleware, so that tests can - * check that some messages actually do (or do not) transit through it. - * - * @author Eric Bottard - */ -public interface Spy { - - Object receive(boolean expectNull) throws Exception; - -} diff --git a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/TestUtils.java b/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/TestUtils.java deleted file mode 100644 index 8be71557a..000000000 --- a/core/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/binder/TestUtils.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.util.Assert; - -/** - * Copy of class in org.springframework.amqp.utils.test to avoid dependency on - * spring-amqp. - * - * @author David Turanski - */ -public abstract class TestUtils { - - /** - * Uses nested {@link DirectFieldAccessor}s to obtain a property using dotted notation - * to traverse fields; e.g. "foo.bar.baz" will obtain a reference to the baz field of - * the bar field of foo. Adopted from Spring Integration. - * @param root The object. - * @param propertyPath The path. - * @return The field. - */ - public static Object getPropertyValue(Object root, String propertyPath) { - Object value = null; - DirectFieldAccessor accessor = new DirectFieldAccessor(root); - String[] tokens = propertyPath.split("\\."); - for (int i = 0; i < tokens.length; i++) { - value = accessor.getPropertyValue(tokens[i]); - if (value != null) { - accessor = new DirectFieldAccessor(value); - } - else if (i == tokens.length - 1) { - return null; - } - else { - throw new IllegalArgumentException( - "intermediate property '" + tokens[i] + "' is null"); - } - } - return value; - } - - @SuppressWarnings("unchecked") - public static T getPropertyValue(Object root, String propertyPath, - Class type) { - Object value = getPropertyValue(root, propertyPath); - if (value != null) { - Assert.isAssignable(type, value.getClass()); - } - return (T) value; - } - -} diff --git a/core/spring-cloud-stream-test-support/src/main/resources/META-INF/spring.factories b/core/spring-cloud-stream-test-support/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 9a0f710e7..000000000 --- a/core/spring-cloud-stream-test-support/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor=\ - org.springframework.cloud.stream.binder.BinderTestEnvironmentPostProcessor diff --git a/core/spring-cloud-stream/.jdk8 b/core/spring-cloud-stream/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/spring-cloud-stream/pom.xml b/core/spring-cloud-stream/pom.xml deleted file mode 100644 index 9fd6a9e53..000000000 --- a/core/spring-cloud-stream/pom.xml +++ /dev/null @@ -1,123 +0,0 @@ - - - 4.0.0 - - spring-cloud-stream - jar - spring-cloud-stream - Messaging Microservices with Spring Integration - - - org.springframework.cloud - spring-cloud-stream-core - 4.1.0-SNAPSHOT - - - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework - spring-messaging - - - org.springframework.integration - spring-integration-core - - - org.springframework.integration - spring-integration-jmx - - - org.springframework.retry - spring-retry - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.cloud - spring-cloud-function-context - - - org.springframework.integration - spring-integration-test - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework - spring-core-test - test - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - org.springframework.integration - spring-integration-http - test - - - org.springframework.boot - spring-boot-starter-web - test - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - - - compile - compile - - compile - - - - src/main/java - src/test/kotlin - - - - - test-compile - test-compile - - test-compile - - - - - 1.8 - - - - - - diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/annotation/StreamRetryTemplate.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/annotation/StreamRetryTemplate.java deleted file mode 100644 index 444aa01a2..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/annotation/StreamRetryTemplate.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; - -/** - * Marker to tag an instance of {@link RetryTemplate} to be used by the binder. This - * annotation is also a @Bean to simplify configuration (see below) - * - *
- * @StreamRetryTemplate
- * public RetryTemplate myRetryTemplate() {
- *    return new RetryTemplate();
- * }
- * 
- * - * @author Oleg Zhurakousky - * @since 2.1 - */ - -@Target({ ElementType.FIELD, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Bean -@Qualifier -public @interface StreamRetryTemplate { - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBinder.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBinder.java deleted file mode 100644 index b99da4dbe..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBinder.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2013-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.cloud.stream.binder; - -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.cloud.stream.annotation.StreamRetryTemplate; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.support.AbstractApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.expression.EvaluationContext; -import org.springframework.integration.expression.ExpressionUtils; -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.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * Base class for {@link Binder} implementations. - * - * @param outbound bind target class - * @param consumer properties class - * @param

producer properties class - * @author David Turanski - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @author Mark Fisher - * @author Marius Bogoevici - * @author Soby Chacko - * @author Vinicius Carvalho - * @author Oleg Zhurakousky - * @author Nicolas Homble - */ -public abstract class AbstractBinder - implements ApplicationContextAware, InitializingBean, Binder { - - /** - * The delimiter between a group and index when constructing a binder - * consumer/producer. - */ - private static final String GROUP_INDEX_DELIMITER = "."; - - protected final Log logger = LogFactory.getLog(getClass()); - - private volatile GenericApplicationContext applicationContext; - - private volatile EvaluationContext evaluationContext; - - @Autowired(required = false) - @StreamRetryTemplate - private Map consumerBindingRetryTemplates; - - /** - * For binder implementations that support a prefix, apply the prefix to the name. - * @param prefix the prefix. - * @param name the name. - * @return name with the prefix - */ - public static String applyPrefix(String prefix, String name) { - return prefix + name; - } - - /** - * For binder implementations that support dead lettering, construct the name of the - * dead letter entity for the underlying pipe name. - * @param name the name. - * @return name with DLQ suffix - */ - public static String constructDLQName(String name) { - return name + ".dlq"; - } - - protected AbstractApplicationContext getApplicationContext() { - return this.applicationContext; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - Assert.isInstanceOf(GenericApplicationContext.class, applicationContext); - this.applicationContext = (GenericApplicationContext) applicationContext; - } - - protected ConfigurableListableBeanFactory getBeanFactory() { - return this.applicationContext.getBeanFactory(); - } - - protected EvaluationContext getEvaluationContext() { - return this.evaluationContext; - } - - @Override - public final void afterPropertiesSet() throws Exception { - Assert.notNull(this.applicationContext, - "The 'applicationContext' property must not be null"); - if (this.evaluationContext == null) { - this.evaluationContext = ExpressionUtils - .createStandardEvaluationContext(getBeanFactory()); - } - onInit(); - } - - /** - * Subclasses may implement this method to perform any necessary initialization. It - * will be invoked from {@link #afterPropertiesSet()} which is itself {@code final}. - * @throws Exception when init fails - */ - protected void onInit() throws Exception { - // no-op default - } - - @Override - public final Binding bindConsumer(String name, String group, T target, - C properties) { - if (!StringUtils.hasText(group)) { - Assert.isTrue(!properties.isPartitioned(), - "A consumer group is required for a partitioned subscription"); - } - return doBindConsumer(name, group, target, properties); - } - - protected abstract Binding doBindConsumer(String name, String group, T inputTarget, - C properties); - - @Override - public final Binding bindProducer(String name, T outboundBindTarget, - P properties) { - return doBindProducer(name, outboundBindTarget, properties); - } - - protected abstract Binding doBindProducer(String name, T outboundBindTarget, - P properties); - - /** - * Construct a name comprised of the name and group. - * @param name the name. - * @param group the group. - * @return the constructed name. - */ - protected final String groupedName(String name, String group) { - return name + GROUP_INDEX_DELIMITER - + (StringUtils.hasText(group) ? group : "default"); - } - - /** - * Attempts to get {@link BindingServiceProperties} from application context. - * - * @return instance of {@link BindingServiceProperties} or null. - */ - protected BindingServiceProperties getBindingServiceProperties() { - try { - return getApplicationContext().getBean(BindingServiceProperties.class); - } - catch (Exception e) { - // ignore - return null; - } - } - - /** - * Create and configure a default retry template unless one has already been provided - * via @Bean by an application. - * @param properties The properties. - * @return The retry template - */ - protected RetryTemplate buildRetryTemplate(ConsumerProperties properties) { - RetryTemplate rt; - if (CollectionUtils.isEmpty(this.consumerBindingRetryTemplates)) { - rt = new RetryTemplate(); - SimpleRetryPolicy retryPolicy = CollectionUtils - .isEmpty(properties.getRetryableExceptions()) - ? new SimpleRetryPolicy(properties.getMaxAttempts()) - : new SimpleRetryPolicy(properties.getMaxAttempts(), - properties.getRetryableExceptions(), true, - properties.isDefaultRetryable()); - - ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); - backOffPolicy.setInitialInterval(properties.getBackOffInitialInterval()); - backOffPolicy.setMultiplier(properties.getBackOffMultiplier()); - backOffPolicy.setMaxInterval(properties.getBackOffMaxInterval()); - rt.setRetryPolicy(retryPolicy); - rt.setBackOffPolicy(backOffPolicy); - } - else { - rt = StringUtils.hasText(properties.getRetryTemplateName()) - ? this.consumerBindingRetryTemplates - .get(properties.getRetryTemplateName()) - : this.consumerBindingRetryTemplates.values().iterator().next(); - } - return rt; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractExtendedBindingProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractExtendedBindingProperties.java deleted file mode 100644 index a4bb0d0ae..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractExtendedBindingProperties.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.BeansException; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; -import org.springframework.boot.context.properties.source.ConfigurationPropertySources; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -/** - * Base implementation of {@link ExtendedBindingProperties}. - * - * @param - consumer properties type - * @param

- producer properties type - * @param - type which provides the consumer and producer properties - * @author Oleg Zhurakousky - * @since 2.1 - */ -public abstract class AbstractExtendedBindingProperties - implements ExtendedBindingProperties, ApplicationContextAware { - - private final Map bindings = new HashMap<>(); - - private ConfigurableApplicationContext applicationContext = new GenericApplicationContext(); - - public void setBindings(Map bindings) { - this.bindings.putAll(bindings); - } - - @SuppressWarnings("unchecked") - @Override - public C getExtendedConsumerProperties(String binding) { - this.bindIfNecessary(binding); - return (C) this.bindings.get(binding).getConsumer(); - } - - @SuppressWarnings("unchecked") - @Override - public P getExtendedProducerProperties(String binding) { - this.bindIfNecessary(binding); - return (P) this.bindings.get(binding).getProducer(); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; - } - - /* - * The "necessary" implies the scenario where only defaults are defined. - */ - private void bindIfNecessary(String bindingName) { - if (!this.bindings.containsKey(bindingName)) { - this.bindToDefault(bindingName); - } - } - - @SuppressWarnings("unchecked") - private void bindToDefault(String binding) { - T extendedBindingPropertiesTarget = (T) BeanUtils - .instantiateClass(this.getExtendedPropertiesEntryClass()); - Binder binder = new Binder( - ConfigurationPropertySources - .get(this.applicationContext.getEnvironment()), - new PropertySourcesPlaceholdersResolver( - this.applicationContext.getEnvironment()), - this.applicationContext.getBeanFactory().getConversionService(), - null); - - binder.bind(this.getDefaultsPrefix(), - Bindable.ofInstance(extendedBindingPropertiesTarget)); - this.bindings.put(binding, extendedBindingPropertiesTarget); - } - - protected Map doGetBindings() { - return Collections.unmodifiableMap(this.bindings); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractMessageChannelBinder.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractMessageChannelBinder.java deleted file mode 100644 index 867cd7dd2..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractMessageChannelBinder.java +++ /dev/null @@ -1,1242 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import org.apache.commons.logging.Log; - -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.config.ConsumerEndpointCustomizer; -import org.springframework.cloud.stream.config.ListenerContainerCustomizer; -import org.springframework.cloud.stream.config.MessageSourceCustomizer; -import org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer; -import org.springframework.cloud.stream.provisioning.ConsumerDestination; -import org.springframework.cloud.stream.provisioning.ProducerDestination; -import org.springframework.cloud.stream.provisioning.ProvisioningException; -import org.springframework.cloud.stream.provisioning.ProvisioningProvider; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.context.Lifecycle; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.expression.Expression; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.channel.AbstractSubscribableChannel; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.integration.channel.PublishSubscribeChannel; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.core.MessageProducer; -import org.springframework.integration.core.MessageSource; -import org.springframework.integration.endpoint.ReactiveStreamsConsumer; -import org.springframework.integration.handler.AbstractMessageHandler; -import org.springframework.integration.handler.BridgeHandler; -import org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer; -import org.springframework.integration.support.ErrorMessageStrategy; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.retry.RecoveryCallback; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * {@link AbstractBinder} that serves as base class for {@link MessageChannel} binders. - * Implementors must implement the following methods: - *

    - *
  • {@link #createProducerMessageHandler(ProducerDestination, ProducerProperties, MessageChannel)}
  • - *
  • {@link #createConsumerEndpoint(ConsumerDestination, String, ConsumerProperties)} - *
  • - *
- * - * @param the consumer properties type - * @param

the producer properties type - * @param the provisioning producer properties type - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Soby Chacko - * @author Oleg Zhurakousky - * @author Artem Bilan - * @author Gary Russell - * @author Byungjun You - * @since 1.1 - */ -public abstract class AbstractMessageChannelBinder> - extends AbstractBinder implements - PollableConsumerBinder, ApplicationEventPublisherAware { - - - /** - * {@link ProvisioningProvider} delegated by the downstream binder implementations. - */ - protected final PP provisioningProvider; - - private final EmbeddedHeadersChannelInterceptor embeddedHeadersChannelInterceptor = new EmbeddedHeadersChannelInterceptor( - this.logger); - - private volatile ObjectMapper objectMapper; - - /** - * Indicates which headers are to be embedded in the payload if a binding requires - * embedding headers. - */ - private final String[] headersToEmbed; - - private ListenerContainerCustomizer containerCustomizer; - - private final MessageSourceCustomizer sourceCustomizer; - - private ProducerMessageHandlerCustomizer handlerCustomizer = - (handler, destination) -> { }; - - private ConsumerEndpointCustomizer consumerCustomizer = - (adapter, destination, group) -> { }; - - private ApplicationEventPublisher applicationEventPublisher; - - public AbstractMessageChannelBinder(String[] headersToEmbed, - PP provisioningProvider) { - this(headersToEmbed, provisioningProvider, null, null); - } - - @Override - protected void onInit() throws Exception { - if (!CollectionUtils.isEmpty(this.getApplicationContext().getBeansOfType(ObjectMapper.class))) { - this.objectMapper = this.getApplicationContext().getBean(ObjectMapper.class); - } - else { - this.objectMapper = new ObjectMapper(); - } - SimpleModule module = new SimpleModule(); - module.addSerializer(Expression.class, new ExpressionSerializer(Expression.class)); - this.objectMapper.registerModule(module); - } - - public AbstractMessageChannelBinder(String[] headersToEmbed, PP provisioningProvider, - @Nullable ListenerContainerCustomizer containerCustomizer, - @Nullable MessageSourceCustomizer sourceCustomizer) { - - this.headersToEmbed = headersToEmbed == null ? new String[0] : headersToEmbed; - this.provisioningProvider = provisioningProvider; - this.containerCustomizer = containerCustomizer == null ? (c, q, g) -> { - } : containerCustomizer; - this.sourceCustomizer = sourceCustomizer == null ? (s, q, g) -> { - } : sourceCustomizer; - } - - protected ApplicationEventPublisher getApplicationEventPublisher() { - return this.applicationEventPublisher; - } - - @Override - public void setApplicationEventPublisher( - ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - /** - * Configure an optional {@link ProducerMessageHandlerCustomizer} for further - * configuration of producer {@link MessageHandler} instances created by the binder. - * @param handlerCustomizer the {@link ProducerMessageHandlerCustomizer} to use. - * @since 3.0 - */ - @SuppressWarnings("unchecked") - public void setProducerMessageHandlerCustomizer( - @Nullable ProducerMessageHandlerCustomizer handlerCustomizer) { - - this.handlerCustomizer = - handlerCustomizer == null - ? (handler, destination) -> { } - : (ProducerMessageHandlerCustomizer) handlerCustomizer; - } - - /** - * Configure an optional {@link ConsumerEndpointCustomizer} for further - * configuration of consumer {@link MessageProducer} instances created by the binder. - * @param endpointCustomizer the {@link ConsumerEndpointCustomizer} to use. - * @since 3.0 - */ - @SuppressWarnings("unchecked") - public void setConsumerEndpointCustomizer( - @Nullable ConsumerEndpointCustomizer endpointCustomizer) { - - this.consumerCustomizer = - endpointCustomizer == null - ? (handler, destination, group) -> { } - : (ConsumerEndpointCustomizer) endpointCustomizer; - } - - /** - * Configure an optional {@link ListenerContainerCustomizer} for further - * configuration of the listener container instance created by the binder. - * @param containerCustomizer the {@link ListenerContainerCustomizer} to use. - */ - public void setContainerCustomizer(@Nullable ListenerContainerCustomizer containerCustomizer) { - - this.containerCustomizer = - containerCustomizer == null - ? (container, destinationName, group) -> { } - : containerCustomizer; - } - - @SuppressWarnings("unchecked") - protected ListenerContainerCustomizer getContainerCustomizer() { - return (ListenerContainerCustomizer) this.containerCustomizer; - } - - @SuppressWarnings("unchecked") - protected MessageSourceCustomizer getMessageSourceCustomizer() { - return (MessageSourceCustomizer) this.sourceCustomizer; - } - - private String resolveBinderName(String bindingName, BindingServiceProperties bindingServiceProperties) { - String binder = bindingServiceProperties == null ? null : bindingServiceProperties.getBindings().get(bindingName).getBinder(); - if (!StringUtils.hasText(binder)) { - return resolveFromDefaultBinder(); - } - return binder; - } - - private String resolveFromDefaultBinder() { - DefaultBinderTypeRegistry binderTypeRegistry = - AbstractMessageChannelBinder.this.getApplicationContext().getBean(DefaultBinderTypeRegistry.class); - Map binderTypes = binderTypeRegistry.getAll(); - if (binderTypes.entrySet().size() > 1 && getBindingServiceProperties().getDefaultBinder() != null) { - return getBindingServiceProperties().getDefaultBinder(); - } - Assert.isTrue(binderTypes.entrySet().size() == 1, "More than one binder types found, but no binder specified on the binding"); - return binderTypes.keySet().iterator().next(); - } - - private String resolveBinderType(String bindingName, BindingServiceProperties bindingServiceProperties) { - String binder = bindingServiceProperties == null ? null : bindingServiceProperties.getBindings().get(bindingName).getBinder(); - if (!StringUtils.hasText(binder)) { - return resolveFromDefaultBinder(); - } - else { - if (bindingServiceProperties.getBinders().get(binder) == null) { - return binder; - } - return bindingServiceProperties.getBinders().get(binder).getType(); - } - } - - /** - * Binds an outbound channel to a given destination. The implementation delegates to - * {@link ProvisioningProvider#provisionProducerDestination(String, ProducerProperties)} - * and - * {@link #createProducerMessageHandler(ProducerDestination, ProducerProperties, MessageChannel)} - * for handling the middleware specific logic. If the returned producer message - * handler is an {@link InitializingBean} then - * {@link InitializingBean#afterPropertiesSet()} will be called on it. Similarly, if - * the returned producer message handler endpoint is a {@link Lifecycle}, then - * {@link Lifecycle#start()} will be called on it. - * @param destination the name of the destination - * @param outputChannel the channel to be bound - * @param producerProperties the {@link ProducerProperties} of the binding - * @return the Binding for the channel - * @throws BinderException on internal errors during binding - */ - @Override - public final Binding doBindProducer(final String destination, - MessageChannel outputChannel, final P producerProperties) - throws BinderException { - final MessageHandler producerMessageHandler; - final ProducerDestination producerDestination; - try { - producerDestination = this.provisioningProvider - .provisionProducerDestination(destination, producerProperties); - - - BindingProperties bp = null; - BindingServiceProperties bsp = this.getBindingServiceProperties(); - if (bsp != null) { - String bindingName = StringUtils.hasText(producerProperties.getBindingName()) ? producerProperties.getBindingName() : destination; - bp = bsp.getBindingProperties(bindingName); - } - - SubscribableChannel errorChannel = (bp != null && StringUtils.hasText(bp.getErrorHandlerDefinition())) || producerProperties.isErrorChannelEnabled() - ? registerErrorInfrastructure(producerDestination, producerProperties.getBindingName()) : null; - - producerMessageHandler = createProducerMessageHandler(producerDestination, - producerProperties, outputChannel, errorChannel); - customizeProducerMessageHandler(producerMessageHandler, producerDestination.getName()); - if (producerMessageHandler instanceof InitializingBean initializingHandler) { - initializingHandler.afterPropertiesSet(); - } - } - catch (Exception e) { - if (e instanceof BinderException binderException) { - throw binderException; - } - else if (e instanceof ProvisioningException provisioningException) { - throw provisioningException; - } - else { - throw new BinderException( - "Exception thrown while building outbound endpoint", e); - } - } - - if (producerProperties.isAutoStartup() - && producerMessageHandler instanceof Lifecycle ProducerMessageHandlerWithLifeCycle) { - ProducerMessageHandlerWithLifeCycle.start(); - } - this.postProcessOutputChannel(outputChannel, producerProperties); - - AtomicReference reactiveStreamsConsumerRef = new AtomicReference<>(); - - if (outputChannel instanceof SubscribableChannel subscribableOutputChannel) { - subscribableOutputChannel - .subscribe(new SendingHandler(producerMessageHandler, - HeaderMode.embeddedHeaders - .equals(producerProperties.getHeaderMode()), - this.headersToEmbed, useNativeEncoding(producerProperties))); - } - else if (outputChannel instanceof FluxMessageChannel) { - final ReactiveStreamsConsumer reactiveStreamsConsumer = new ReactiveStreamsConsumer(outputChannel, producerMessageHandler); - reactiveStreamsConsumerRef.set(reactiveStreamsConsumer); - reactiveStreamsConsumer.start(); - } - else { - throw new IllegalStateException("No capable binding targets found."); - } - - - BindingServiceProperties bsp = this.getBindingServiceProperties(); - Binding binding = new DefaultBinding(destination, - outputChannel, producerMessageHandler instanceof Lifecycle producerMessageHandlerWithLifecycle - ? producerMessageHandlerWithLifecycle : null) { - - @Override - public Map getExtendedInfo() { - return doGetExtendedInfo(destination, producerProperties); - } - - @Override - public boolean isInput() { - return false; - } - - @Override - public String getBinderName() { - return resolveBinderName(getBindingName(), bsp); - } - - @Override - public String getBinderType() { - return resolveBinderType(getBindingName(), bsp); - } - - @Override - public void afterUnbind() { - try { - destroyErrorInfrastructure(producerDestination, producerProperties.getBindingName()); - final ReactiveStreamsConsumer rsc = reactiveStreamsConsumerRef.get(); - if (rsc != null && rsc.isRunning()) { - rsc.destroy(); - } - if (producerMessageHandler instanceof DisposableBean disposableProducerMessageHandler) { - disposableProducerMessageHandler.destroy(); - } - } - catch (Exception e) { - AbstractMessageChannelBinder.this.logger - .error("Exception thrown while unbinding " + toString(), e); - } - afterUnbindProducer(producerDestination, producerProperties); - } - }; - - Lifecycle companion = null; - String outputChannelName = ((AbstractMessageChannel) outputChannel).getBeanName(); - String companionLifecycleName = outputChannelName + "_spca"; - if (this.getApplicationContext().containsBean(companionLifecycleName)) { - companion = this.getApplicationContext().getBean(companionLifecycleName, Lifecycle.class); - } - ((DefaultBinding) binding).setCompanion(companion); - - doPublishEvent(new BindingCreatedEvent(binding)); - return binding; - } - - protected void customizeProducerMessageHandler(MessageHandler producerMessageHandler, String destinationName) { - this.handlerCustomizer.configure(producerMessageHandler, destinationName); - } - - /** - * Whether the producer for the destination being created should be configured to use - * native encoding which may, or may not, be determined from the properties. For - * example, a transactional kafka binder uses a common producer for all destinations. - * The default implementation returns {@link ProducerProperties#isUseNativeEncoding()}. - * @param producerProperties the properties. - * @return true to use native encoding. - */ - protected boolean useNativeEncoding(P producerProperties) { - return producerProperties.isUseNativeEncoding(); - } - - /** - * Allows subclasses to perform post processing on the channel - for example to add - * more interceptors. - * @param outputChannel the channel. - * @param producerProperties the producer properties. - */ - protected void postProcessOutputChannel(MessageChannel outputChannel, - P producerProperties) { - // default no-op - } - - /** - * Create a {@link MessageHandler} with the ability to send data to the target - * middleware. If the returned instance is also a {@link Lifecycle}, it will be - * stopped automatically by the binder. - *

- * In order to be fully compliant, the {@link MessageHandler} of the binder must - * observe the following headers: - *

    - *
  • {@link BinderHeaders#PARTITION_HEADER} - indicates the target partition where - * the message must be sent
  • - *
- *

- * @param destination the name of the target destination. - * @param producerProperties the producer properties. - * @param channel the channel to bind. - * @param errorChannel the error channel (if enabled, otherwise null). If not null, - * the binder must wire this channel into the producer endpoint so that errors are - * forwarded to it. - * @return the message handler for sending data to the target middleware - * @throws Exception when producer messsage handler failed to be created - */ - protected MessageHandler createProducerMessageHandler(ProducerDestination destination, - P producerProperties, MessageChannel channel, MessageChannel errorChannel) - throws Exception { - return createProducerMessageHandler(destination, producerProperties, - errorChannel); - } - - /** - * Create a {@link MessageHandler} with the ability to send data to the target - * middleware. If the returned instance is also a {@link Lifecycle}, it will be - * stopped automatically by the binder. - *

- * In order to be fully compliant, the {@link MessageHandler} of the binder must - * observe the following headers: - *

    - *
  • {@link BinderHeaders#PARTITION_HEADER} - indicates the target partition where - * the message must be sent
  • - *
- *

- * @param destination the name of the target destination - * @param producerProperties the producer properties - * @param errorChannel the error channel (if enabled, otherwise null). If not null, - * the binder must wire this channel into the producer endpoint so that errors are - * forwarded to it. - * @return the message handler for sending data to the target middleware - * @throws Exception upon failure to create the producer message handler - */ - protected abstract MessageHandler createProducerMessageHandler( - ProducerDestination destination, P producerProperties, - MessageChannel errorChannel) throws Exception; - - /** - * Invoked after the unbinding of a producer. Subclasses may override this to provide - * their own logic for dealing with unbinding. - * @param destination the bound destination - * @param producerProperties the producer properties - */ - protected void afterUnbindProducer(ProducerDestination destination, - P producerProperties) { - } - - /** - * Binds an inbound channel to a given destination. The implementation delegates to - * {@link ProvisioningProvider#provisionConsumerDestination(String, String, ConsumerProperties)} - * and - * {@link #createConsumerEndpoint(ConsumerDestination, String, ConsumerProperties)} - * for handling middleware-specific logic. If the returned consumer endpoint is an - * {@link InitializingBean} then {@link InitializingBean#afterPropertiesSet()} will be - * called on it. Similarly, if the returned consumer endpoint is a {@link Lifecycle}, - * then {@link Lifecycle#start()} will be called on it. - * @param name the name of the destination - * @param group the consumer group - * @param inputChannel the channel to be bound - * @param properties the {@link ConsumerProperties} of the binding - * @return the Binding for the channel - * @throws BinderException on internal errors during binding - */ - @Override - public final Binding doBindConsumer(String name, String group, - MessageChannel inputChannel, final C properties) throws BinderException { - MessageProducer consumerEndpoint = null; - try { - ConsumerDestination destination = this.provisioningProvider - .provisionConsumerDestination(name, group, properties); - - if (HeaderMode.embeddedHeaders.equals(properties.getHeaderMode())) { - enhanceMessageChannel(inputChannel); - } - consumerEndpoint = createConsumerEndpoint(destination, group, properties); - consumerEndpoint.setOutputChannel(inputChannel); - this.consumerCustomizer.configure(consumerEndpoint, name, group); - if (consumerEndpoint instanceof InitializingBean initializingConsumerEndpoint) { - initializingConsumerEndpoint.afterPropertiesSet(); - } - if (properties.isAutoStartup() && consumerEndpoint instanceof Lifecycle consumerEndpointWithLifecycle) { - consumerEndpointWithLifecycle.start(); - } - - BindingServiceProperties bsp = this.getBindingServiceProperties(); - Binding binding = new DefaultBinding(name, - group, inputChannel, consumerEndpoint instanceof Lifecycle consumerEndpointWithLifecycle - ? consumerEndpointWithLifecycle : null) { - - @Override - public Map getExtendedInfo() { - return doGetExtendedInfo(destination, properties); - } - - @Override - public boolean isInput() { - return true; - } - - @Override - public String getBinderName() { - return resolveBinderName(getBindingName(), bsp); - } - - @Override - public String getBinderType() { - return resolveBinderType(getBindingName(), bsp); - } - - @Override - protected void afterUnbind() { - try { - if (getEndpoint() instanceof DisposableBean disposableEndpoint) { - disposableEndpoint.destroy(); - } - } - catch (Exception e) { - AbstractMessageChannelBinder.this.logger.error( - "Exception thrown while unbinding " + toString(), e); - } - afterUnbindConsumer(destination, this.group, properties); - destroyErrorInfrastructure(destination, this.group, properties); - } - - }; - doPublishEvent(new BindingCreatedEvent(binding)); - return binding; - } - catch (Exception e) { - if (consumerEndpoint instanceof Lifecycle consumerEndpointWithLifecycle) { - consumerEndpointWithLifecycle.stop(); - } - if (e instanceof BinderException binderException) { - throw binderException; - } - else if (e instanceof ProvisioningException provisioningException) { - throw provisioningException; - } - else { - throw new BinderException("Exception thrown while starting consumer: ", e); - } - } - } - - @Override - public Binding> bindPollableConsumer(String name, - String group, final PollableSource inboundBindTarget, - C properties) { - Assert.isInstanceOf(DefaultPollableMessageSource.class, inboundBindTarget); - DefaultPollableMessageSource bindingTarget = (DefaultPollableMessageSource) inboundBindTarget; - ConsumerDestination destination = this.provisioningProvider - .provisionConsumerDestination(name, group, properties); - if (HeaderMode.embeddedHeaders.equals(properties.getHeaderMode())) { - bindingTarget.addInterceptor(0, this.embeddedHeadersChannelInterceptor); - } - final PolledConsumerResources resources = createPolledConsumerResources(name, - group, destination, properties); - - MessageSource messageSource = resources.getSource(); - if (messageSource instanceof BeanFactoryAware beanFactoryAwareMessageSource) { - beanFactoryAwareMessageSource.setBeanFactory(getApplicationContext().getBeanFactory()); - } - bindingTarget.setSource(messageSource); - if (resources.getErrorInfrastructure() != null) { - if (resources.getErrorInfrastructure().getErrorChannel() != null) { - bindingTarget.setErrorChannel( - resources.getErrorInfrastructure().getErrorChannel()); - } - ErrorMessageStrategy ems = getErrorMessageStrategy(); - if (ems != null) { - bindingTarget.setErrorMessageStrategy(ems); - } - } - if (properties.getMaxAttempts() > 1) { - bindingTarget.setRetryTemplate(buildRetryTemplate(properties)); - bindingTarget.setRecoveryCallback(getPolledConsumerRecoveryCallback( - resources.getErrorInfrastructure(), properties)); - } - postProcessPollableSource(bindingTarget); - if (properties.isAutoStartup() && resources.getSource() instanceof Lifecycle sourceWithLifecycle) { - sourceWithLifecycle.start(); - } - - BindingServiceProperties bsp = this.getBindingServiceProperties(); - Binding> binding = new DefaultBinding>( - name, group, inboundBindTarget, resources.getSource() instanceof Lifecycle sourceWithLifecycle - ? sourceWithLifecycle : null) { - - @Override - public Map getExtendedInfo() { - return doGetExtendedInfo(destination, properties); - } - - @Override - public boolean isInput() { - return true; - } - - @Override - public String getBinderName() { - return resolveBinderName(getBindingName(), bsp); - } - - @Override - public String getBinderType() { - return resolveBinderType(getBindingName(), bsp); - } - - @Override - public void afterUnbind() { - afterUnbindConsumer(destination, this.group, properties); - destroyErrorInfrastructure(destination, this.group, properties); - } - - }; - - doPublishEvent(new BindingCreatedEvent(binding)); - return binding; - } - - protected void postProcessPollableSource(DefaultPollableMessageSource bindingTarget) { - } - - /** - * Implementations can override the default {@link ErrorMessageSendingRecoverer}. - * @param errorInfrastructure the infrastructure. - * @param properties the consumer properties. - * @return the recoverer. - */ - protected RecoveryCallback getPolledConsumerRecoveryCallback( - ErrorInfrastructure errorInfrastructure, C properties) { - return errorInfrastructure.getRecoverer(); - } - - protected PolledConsumerResources createPolledConsumerResources(String name, - String group, ConsumerDestination destination, C consumerProperties) { - throw new UnsupportedOperationException( - "This binder does not support pollable consumers"); - } - - private void enhanceMessageChannel(MessageChannel inputChannel) { - ((AbstractMessageChannel) inputChannel).addInterceptor(0, - this.embeddedHeadersChannelInterceptor); - } - - /** - * Creates {@link MessageProducer} that receives data from the consumer destination. - * will be started and stopped by the binder. - * @param group the consumer group - * @param destination reference to the consumer destination - * @param properties the consumer properties - * @return the consumer endpoint. - * @throws Exception when consumer endpoint creation failed. - */ - protected abstract MessageProducer createConsumerEndpoint( - ConsumerDestination destination, String group, C properties) throws Exception; - - /** - * Invoked after the unbinding of a consumer. The binder implementation can override - * this method to provide their own logic (e.g. for cleaning up destinations). - * @param destination the consumer destination - * @param group the consumer group - * @param consumerProperties the consumer properties - */ - protected void afterUnbindConsumer(ConsumerDestination destination, String group, - C consumerProperties) { - } - - /** - * Register an error channel for the destination when an async send error is received. - * Bridge the channel to the global error channel (if present). - * @param destination the destination. - * @return the channel. - */ - private SubscribableChannel registerErrorInfrastructure( - ProducerDestination destination, String bindingName) { - - String errorChannelName = errorsBaseName(destination, bindingName); - SubscribableChannel errorChannel = new PublishSubscribeChannel(); - - if (getApplicationContext().containsBean(errorChannelName)) { - Object errorChannelObject = getApplicationContext().getBean(errorChannelName); - if (!(errorChannelObject instanceof SubscribableChannel)) { - throw new IllegalStateException("Error channel '" + errorChannelName - + "' must be a SubscribableChannel"); - } - if (errorChannelObject instanceof DirectChannel) { - errorChannelName = "bridged." + errorChannelName; - BridgeHandler bridge = new BridgeHandler(); - bridge.setOutputChannel((MessageChannel) errorChannelObject); - errorChannel.subscribe(bridge); - } - } - else { - ((GenericApplicationContext) getApplicationContext()).registerBean( - errorChannelName, SubscribableChannel.class, () -> errorChannel); - } - - MessageChannel defaultErrorChannel = null; - if (getApplicationContext() - .containsBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME)) { - defaultErrorChannel = getApplicationContext().getBean( - IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, - MessageChannel.class); - } - if (defaultErrorChannel != null) { - BridgeHandler errorBridge = new BridgeHandler(); - errorBridge.setOutputChannel(defaultErrorChannel); - errorChannel.subscribe(errorBridge); - String errorBridgeHandlerName = getErrorBridgeName(destination, bindingName); - ((GenericApplicationContext) getApplicationContext()).registerBean( - errorBridgeHandlerName, BridgeHandler.class, () -> errorBridge); - } - return errorChannel; - } - - /** - * Build an errorChannelRecoverer that writes to a pub/sub channel for the destination - * when an exception is thrown to a consumer. - * @param destination the destination. - * @param group the group. - * @param consumerProperties the properties. - * @return the ErrorInfrastructure which is a holder for the error channel, the - * recoverer and the message handler that is subscribed to the channel. - */ - protected final ErrorInfrastructure registerErrorInfrastructure( - ConsumerDestination destination, String group, C consumerProperties) { - - return registerErrorInfrastructure(destination, group, consumerProperties, false); - } - - private boolean subscribeFunctionErrorHandler(String errorChannelName, String bindingName) { - if (!StringUtils.hasText(bindingName)) { - return false; - } - BindingServiceProperties bsp = this.getBindingServiceProperties(); - if (bsp != null) { - BindingProperties bp = bsp.getBindingProperties(bindingName); - if (bp != null && StringUtils.hasText(bp.getErrorHandlerDefinition())) { - FunctionCatalog catalog = getApplicationContext().getBean(FunctionCatalog.class); - Consumer errorHandler = catalog.lookup(Consumer.class, bp.getErrorHandlerDefinition()); - if (errorHandler == null) { - logger.warn("Failed to retrieve error handling function with definition: " + bp.getErrorHandlerDefinition() + ", for binding: " + bindingName); - return false; - } - else { - SubscribableChannel functionErrorChannel = getApplicationContext().getBean(errorChannelName, SubscribableChannel.class); - functionErrorChannel.subscribe(errorMessage -> errorHandler.accept((ErrorMessage) errorMessage)); - return true; - } - } - } - return false; - } - - /** - * Build an errorChannelRecoverer that writes to a pub/sub channel for the destination - * when an exception is thrown to a consumer. - * @param destination the destination. - * @param group the group. - * @param consumerProperties the properties. - * @param polled true if this is for a polled consumer. - * @return the ErrorInfrastructure which is a holder for the error channel, the - * recoverer and the message handler that is subscribed to the channel. - */ - protected final ErrorInfrastructure registerErrorInfrastructure( - ConsumerDestination destination, String group, C consumerProperties, - boolean polled) { - - ErrorMessageStrategy errorMessageStrategy = getErrorMessageStrategy(); - String errorChannelName = errorsBaseName(destination, group, consumerProperties); - BindingServiceProperties bsp = this.getBindingServiceProperties(); - FunctionInvocationWrapper userErrorHandler = null; - String errorHandlerDefinition = null; - if (bsp != null && StringUtils.hasText(consumerProperties.getBindingName())) { - BindingProperties bp = bsp.getBindingProperties(consumerProperties.getBindingName()); - errorHandlerDefinition = bp.getErrorHandlerDefinition(); - FunctionCatalog catalog = getApplicationContext().getBean(FunctionCatalog.class); - if (StringUtils.hasText(errorHandlerDefinition)) { - userErrorHandler = catalog.lookup(errorHandlerDefinition); - if (!(userErrorHandler != null && userErrorHandler.getFunctionDefinition().equals(errorHandlerDefinition))) { - userErrorHandler = null; - } - } - } - - AbstractSubscribableChannel binderErrorChannel; - if (this.getApplicationContext().containsBean(errorChannelName)) { - binderErrorChannel = this.getApplicationContext().getBean(errorChannelName, AbstractSubscribableChannel.class); - } - else { - binderErrorChannel = new BinderErrorChannel(); - binderErrorChannel.setComponentName(errorChannelName); - ((GenericApplicationContext) getApplicationContext()).registerBean( - errorChannelName, SubscribableChannel.class, () -> binderErrorChannel); - } - - boolean userHandlerSubscribed = this.subscribeFunctionErrorHandler(errorChannelName, consumerProperties.getBindingName()); - - ErrorMessageSendingRecoverer recoverer = new ErrorMessageSendingRecoverer(binderErrorChannel, errorMessageStrategy); - String recovererBeanName = getErrorRecovererName(destination, group, consumerProperties); - if (!getApplicationContext().containsBean(recovererBeanName)) { - ((GenericApplicationContext) getApplicationContext()).registerBean( - recovererBeanName, ErrorMessageSendingRecoverer.class, () -> recoverer); - } - - MessageHandler binderProvidedErrorHandler = polled - ? getPolledConsumerErrorMessageHandler(destination, group, consumerProperties) - : getErrorMessageHandler(destination, group, consumerProperties); - - String errorMessageHandlerName = getErrorMessageHandlerName(destination, group, - consumerProperties); - - if (binderProvidedErrorHandler == null) { - binderProvidedErrorHandler = this.getDefaultErrorMessageHandler(binderErrorChannel, polled); - } - - if (binderProvidedErrorHandler != null && !userHandlerSubscribed) { - if (this.isSubscribable(binderErrorChannel) && !getApplicationContext().containsBean(errorMessageHandlerName)) { - MessageHandler h = binderProvidedErrorHandler; - ((GenericApplicationContext) getApplicationContext()).registerBean(errorMessageHandlerName, MessageHandler.class, () -> h); - binderErrorChannel.subscribe(binderProvidedErrorHandler); - } - else { - this.logger.warn("The provided errorChannel '" + errorChannelName - + "' is an instance of DirectChannel, " - + "so no more subscribers could be added which may affect DLQ processing. " - + "Resolution: Configure your own errorChannel as " - + "an instance of PublishSubscribeChannel"); - } - } - - // Setup a bridge to global errorChannel to ensure logging of errors could be controlled via standard SI way - if (this.getApplicationContext().containsBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME) && this.isSubscribable(binderErrorChannel)) { - SubscribableChannel globalErrorChannel = this.getApplicationContext().getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, SubscribableChannel.class); - BridgeHandler bridge = new BridgeHandler(); - bridge.setOutputChannel(globalErrorChannel); - binderErrorChannel.subscribe(bridge); - } - return new ErrorInfrastructure(binderErrorChannel, recoverer, binderProvidedErrorHandler); - } - - private boolean isSubscribable(SubscribableChannel errorChannel) { - if (errorChannel instanceof PublishSubscribeChannel) { - return true; - } - return !(errorChannel instanceof AbstractSubscribableChannel subscribableErrorChannel) - || subscribableErrorChannel.getSubscriberCount() == 0; - } - - private void destroyErrorInfrastructure(ProducerDestination destination, String bindingName) { - String errorChannelName = errorsBaseName(destination, bindingName); - String errorBridgeHandlerName = getErrorBridgeName(destination, bindingName); - MessageHandler bridgeHandler = null; - if (getApplicationContext().containsBean(errorBridgeHandlerName)) { - bridgeHandler = getApplicationContext().getBean(errorBridgeHandlerName, - MessageHandler.class); - } - if (getApplicationContext().containsBean(errorChannelName)) { - SubscribableChannel channel = getApplicationContext() - .getBean(errorChannelName, SubscribableChannel.class); - if (bridgeHandler != null) { - channel.unsubscribe(bridgeHandler); - ((DefaultSingletonBeanRegistry) getApplicationContext().getBeanFactory()) - .destroySingleton(errorBridgeHandlerName); - } - ((DefaultSingletonBeanRegistry) getApplicationContext().getBeanFactory()) - .destroySingleton(errorChannelName); - } - } - - private void destroyErrorInfrastructure(ConsumerDestination destination, String group, - C properties) { - try { - String recoverer = getErrorRecovererName(destination, group, properties); - - destroyBean(recoverer); - - String errorChannelName = errorsBaseName(destination, group, properties); - String errorMessageHandlerName = getErrorMessageHandlerName(destination, - group, properties); - String errorBridgeHandlerName = getErrorBridgeName(destination, group, - properties); - MessageHandler bridgeHandler = null; - if (getApplicationContext().containsBean(errorBridgeHandlerName)) { - bridgeHandler = getApplicationContext().getBean(errorBridgeHandlerName, - MessageHandler.class); - } - MessageHandler handler = null; - if (getApplicationContext().containsBean(errorMessageHandlerName)) { - handler = getApplicationContext().getBean(errorMessageHandlerName, - MessageHandler.class); - } - if (getApplicationContext().containsBean(errorChannelName)) { - SubscribableChannel channel = getApplicationContext() - .getBean(errorChannelName, SubscribableChannel.class); - if (bridgeHandler != null) { - channel.unsubscribe(bridgeHandler); - destroyBean(errorBridgeHandlerName); - } - if (handler != null) { - channel.unsubscribe(handler); - destroyBean(errorMessageHandlerName); - } - destroyBean(errorChannelName); - } - } - catch (IllegalStateException e) { - // context is shutting down. - } - } - - private void destroyBean(String beanName) { - if (getApplicationContext().containsBeanDefinition(beanName)) { - ((DefaultSingletonBeanRegistry) getApplicationContext().getBeanFactory()) - .destroySingleton(beanName); - ((GenericApplicationContext) getApplicationContext()) - .removeBeanDefinition(beanName); - } - } - - /** - * Binders can return a message handler to be subscribed to the error channel. - * Examples might be if the user wishes to (re)publish messages to a DLQ. - * @param destination the destination. - * @param group the group. - * @param consumerProperties the properties. - * @return the handler (may be null, which is the default, causing the exception to be - * rethrown). - */ - protected MessageHandler getErrorMessageHandler(ConsumerDestination destination, - String group, C consumerProperties) { - return null; - } - - /** - * Binders can return a message handler to be subscribed to the error channel. - * Examples might be if the user wishes to (re)publish messages to a DLQ. - * @param destination the destination. - * @param group the group. - * @param consumerProperties the properties. - * @return the handler (may be null, which is the default, causing the exception to be - * rethrown). - */ - protected MessageHandler getPolledConsumerErrorMessageHandler( - ConsumerDestination destination, String group, C consumerProperties) { - return null; - } - - /** - * Return the default error message handler, which throws the error message payload to - * the caller if there are no user handlers subscribed. The handler is ordered so it - * runs after any user-defined handlers that are subscribed. - * @param errorChannel the error channel. - * @param defaultErrorChannelPresent true if the context has a default 'errorChannel'. - * @return the handler. - */ - protected MessageHandler getDefaultErrorMessageHandler( - SubscribableChannel errorChannel, boolean defaultErrorChannelPresent) { - return new FinalRethrowingErrorMessageHandler(); - } - - /** - * Binders can return an {@link ErrorMessageStrategy} for building error messages; - * binder implementations typically might add extra headers to the error message. - * @return the implementation - may be null. - */ - protected ErrorMessageStrategy getErrorMessageStrategy() { - return null; - } - - protected String getErrorRecovererName(ConsumerDestination destination, String group, - C consumerProperties) { - return errorsBaseName(destination, group, consumerProperties) + ".recoverer"; - } - - protected String getErrorMessageHandlerName(ConsumerDestination destination, - String group, C consumerProperties) { - return errorsBaseName(destination, group, consumerProperties) + ".handler"; - } - - protected String getErrorBridgeName(ConsumerDestination destination, String group, - C consumerProperties) { - return errorsBaseName(destination, group, consumerProperties) + ".bridge"; - } - - protected String errorsBaseName(ConsumerDestination destination, String group, - C consumerProperties) { - return this.doErrorBaseName(consumerProperties.getBindingName()); - } - - protected String getErrorBridgeName(ProducerDestination destination, String bindingName) { - return errorsBaseName(destination, bindingName) + ".bridge" + destination.hashCode(); - } - - protected String errorsBaseName(ProducerDestination destination, String bindingName) { - return this.doErrorBaseName(bindingName); - } - - private String doErrorBaseName(String bindingName) { - return this.getBinderIdentity() + "." + bindingName + ".errors"; - } - - private Map doGetExtendedInfo(Object destination, Object properties) { - Map extendedInfo = new LinkedHashMap<>(); - extendedInfo.put("bindingDestination", destination.toString()); - extendedInfo.put(properties.getClass().getSimpleName(), - this.objectMapper.convertValue(properties, Map.class)); - return extendedInfo; - } - - private void doPublishEvent(ApplicationEvent event) { - if (this.applicationEventPublisher != null) { - try { - this.applicationEventPublisher.publishEvent(event); - } - catch (Exception e) { - logger.warn("Failed while publishing event " + event + ". " - + "From the framework perspective this is harmless and typically " - + "happens when use implement custom ApplicationListener"); - logger.debug(e); - } - } - } - - protected static class ErrorInfrastructure { - - private final SubscribableChannel errorChannel; - - private final ErrorMessageSendingRecoverer recoverer; - - private final MessageHandler handler; - - ErrorInfrastructure(SubscribableChannel errorChannel, - ErrorMessageSendingRecoverer recoverer, MessageHandler handler) { - this.errorChannel = errorChannel; - this.recoverer = recoverer; - this.handler = handler; - } - - public SubscribableChannel getErrorChannel() { - return this.errorChannel; - } - - public ErrorMessageSendingRecoverer getRecoverer() { - return this.recoverer; - } - - public MessageHandler getHandler() { - return this.handler; - } - - } - - private static final class EmbeddedHeadersChannelInterceptor - implements ChannelInterceptor { - - protected final Log logger; - - EmbeddedHeadersChannelInterceptor(Log logger) { - this.logger = logger; - } - - @Override - @SuppressWarnings("unchecked") - public Message preSend(Message message, MessageChannel channel) { - if (message.getPayload() instanceof byte[] messagePayload - && !message.getHeaders() - .containsKey(BinderHeaders.NATIVE_HEADERS_PRESENT) - && EmbeddedHeaderUtils - .mayHaveEmbeddedHeaders(messagePayload)) { - - MessageValues messageValues; - try { - messageValues = EmbeddedHeaderUtils - .extractHeaders((Message) message, true); - } - catch (Exception e) { - /* - * debug() rather then error() since we don't know for sure that it - * really is a message with embedded headers, it just meets the - * criteria in EmbeddedHeaderUtils.mayHaveEmbeddedHeaders(). - */ - if (this.logger.isDebugEnabled()) { - this.logger.debug( - EmbeddedHeaderUtils.decodeExceptionMessage(message), e); - } - messageValues = new MessageValues(message); - } - return messageValues.toMessage(); - } - return message; - } - - } - - protected static class PolledConsumerResources { - - private final MessageSource source; - - private final ErrorInfrastructure errorInfrastructure; - - public PolledConsumerResources(MessageSource source, - ErrorInfrastructure errorInfrastructure) { - this.source = source; - this.errorInfrastructure = errorInfrastructure; - } - - MessageSource getSource() { - return this.source; - } - - ErrorInfrastructure getErrorInfrastructure() { - return this.errorInfrastructure; - } - - } - - private final class SendingHandler extends AbstractMessageHandler - implements Lifecycle { - - private final boolean embedHeaders; - - private final String[] embeddedHeaders; - - private final MessageHandler delegate; - - private final boolean useNativeEncoding; - - private SendingHandler(MessageHandler delegate, boolean embedHeaders, - String[] headersToEmbed, boolean useNativeEncoding) { - this.delegate = delegate; - setBeanFactory(AbstractMessageChannelBinder.this.getBeanFactory()); - this.embedHeaders = embedHeaders; - this.embeddedHeaders = headersToEmbed; - this.useNativeEncoding = useNativeEncoding; - } - - @Override - protected void handleMessageInternal(Message message) { - Message messageToSend = (this.useNativeEncoding) ? message - : serializeAndEmbedHeadersIfApplicable(message); - this.delegate.handleMessage(messageToSend); - } - - private Message serializeAndEmbedHeadersIfApplicable(Message message) { - MessageValues transformed = new MessageValues(message); - Object payload; - if (this.embedHeaders) { - Object contentType = transformed.get(MessageHeaders.CONTENT_TYPE); - // transform content type headers to String, so that they can be properly - // embedded in JSON - if (contentType != null) { - transformed.put(MessageHeaders.CONTENT_TYPE, contentType.toString()); - } - payload = EmbeddedHeaderUtils.embedHeaders(transformed, - this.embeddedHeaders); - } - else { - payload = transformed.getPayload(); - } - return getMessageBuilderFactory().withPayload(payload) - .copyHeaders(transformed.getHeaders()).build(); - } - - @Override - public void start() { - if (this.delegate instanceof Lifecycle delegateWithLifecycle) { - delegateWithLifecycle.start(); - } - } - - @Override - public void stop() { - if (this.delegate instanceof Lifecycle delegateWithLifecycle) { - delegateWithLifecycle.stop(); - } - } - - @Override - public boolean isRunning() { - return this.delegate instanceof Lifecycle delegateWithLifecycle - && delegateWithLifecycle.isRunning(); - } - - } - - @SuppressWarnings("serial") - private static class ExpressionSerializer extends StdSerializer { - - protected ExpressionSerializer(Class t) { - super(t); - } - - @Override - public void serialize(Expression value, JsonGenerator gen, SerializerProvider provider) throws IOException { - gen.writeString(value.getExpressionString()); - } - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java deleted file mode 100644 index aef0b6df8..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2013-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -/** - * A strategy interface used to bind an app interface to a logical name. The name is - * intended to identify a logical consumer or producer of messages. This may be a queue, a - * channel adapter, another message channel, a Spring bean, etc. - * - * @param the primary binding type (e.g. MessageChannel). - * @param the consumer properties type. - * @param

the producer properties type. - * @author Mark Fisher - * @author David Turanski - * @author Gary Russell - * @author Jennifer Hickey - * @author Ilayaperumal Gopinathan - * @author Marius Bogoevici - * @author Oleg Zhurakousky - * @since 1.0 - */ -public interface Binder { - - /** - * Returns instance identity of this binder. - * Individual binders should normally override this method. - * @return instance identity of this binder - */ - default String getBinderIdentity() { - return String.valueOf(this.hashCode()); - } - - /** - * Bind the target component as a message consumer to the logical entity identified by - * the name. - * @param name the logical identity of the message source - * @param group the consumer group to which this consumer belongs - subscriptions are - * shared among consumers in the same group (a null or empty String, must - * be treated as an anonymous group that doesn't share the subscription with any other - * consumer) - * @param inboundBindTarget the app interface to be bound as a consumer - * @param consumerProperties the consumer properties - * @return the setup binding - */ - Binding bindConsumer(String name, String group, T inboundBindTarget, - C consumerProperties); - - /** - * Bind the target component as a message producer to the logical entity identified by - * the name. - * @param name the logical identity of the message target - * @param outboundBindTarget the app interface to be bound as a producer - * @param producerProperties the producer properties - * @return the setup binding - */ - Binding bindProducer(String name, T outboundBindTarget, P producerProperties); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java deleted file mode 100644 index 8c429fe2f..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderChildContextInitializer.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binder; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.lang.model.element.Modifier; - -import org.apache.commons.logging.LogFactory; - -import org.springframework.aot.generate.GeneratedMethod; -import org.springframework.aot.generate.GenerationContext; -import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; -import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; -import org.springframework.beans.factory.aot.BeanRegistrationCode; -import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.aot.ApplicationContextAotGenerator; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.log.LogAccessor; -import org.springframework.javapoet.ClassName; -import org.springframework.util.Assert; - -/** - * @author Chris Bono - * @since 4.0 - */ -public class BinderChildContextInitializer implements ApplicationContextAware, BeanRegistrationAotProcessor { - - private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); - private DefaultBinderFactory binderFactory; - private final Map> childContextInitializers; - private volatile ConfigurableApplicationContext context; - - public BinderChildContextInitializer() { - this.childContextInitializers = new HashMap<>(); - } - - public BinderChildContextInitializer( - Map> childContextInitializers) { - this.childContextInitializers = childContextInitializers; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext); - this.context = (ConfigurableApplicationContext) applicationContext; - } - - public void setBinderFactory(DefaultBinderFactory binderFactory) { - Assert.notNull(binderFactory, () -> "binderFactory must be non-null"); - this.binderFactory = binderFactory; - if (!this.childContextInitializers.isEmpty()) { - this.logger.debug(() -> "Setting binder child context initializers on binder factory"); - this.binderFactory.setBinderChildContextInitializers(this.childContextInitializers); - } - } - - @Override - public boolean isBeanExcludedFromAotProcessing() { - return false; - } - - @Override - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { - if (registeredBean.getBeanClass().equals(getClass())) { //&& registeredBean.getBeanFactory().equals(this.context)) { - this.logger.debug(() -> "Beginning AOT processing for binder child contexts"); - ensureBinderFactoryIsSet(); - // Load the binding service properties from the environment and update the binder factory with them - // in order to pick up any user-declared binders. Without this step only the default binder defined - // in 'META-INF/spring.binders' will be processed. - BindingServiceProperties declaredBinders = this.createBindingServiceProperties(); - Map binderConfigurations = BindingServiceConfiguration.getBinderConfigurations( - this.binderFactory.getBinderTypeRegistry(), declaredBinders); - this.binderFactory.updateBinderConfigurations(binderConfigurations); - Map binderChildContexts = binderConfigurations.entrySet().stream() - .map(e -> Map.entry(e.getKey(), binderFactory.createBinderContextForAOT(e.getKey()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return new BinderChildContextAotContribution(binderChildContexts); - } - return null; - } - - private BindingServiceProperties createBindingServiceProperties() { - BindingServiceProperties bindingServiceProperties = new BindingServiceProperties(); - Binder.get(this.context.getEnvironment()) - .bind("spring.cloud.stream", Bindable.ofInstance(bindingServiceProperties)); - return bindingServiceProperties; - } - - private void ensureBinderFactoryIsSet() { - if (this.binderFactory == null) { - Assert.notNull(this.context, () -> "Unable to lookup binder factory from context as this.context is null"); - this.binderFactory = this.context.getBean(DefaultBinderFactory.class); - } - } - - /** - * Callback for AOT generated {@link BinderChildContextAotContribution#applyTo(GenerationContext, BeanRegistrationCode) - * post-process method} which basically swaps the instance with one that uses the AOT generated child context - * initializers. - * @param childContextInitializers the child context initializers to use - * @return copy of this instance that uses the AOT generated child context initializers - */ - @SuppressWarnings({"unused", "raw"}) - public BinderChildContextInitializer withChildContextInitializers( - Map> childContextInitializers) { - this.logger.debug(() -> "Replacing instance w/ one that uses child context initializers"); - Map> downcastedInitializers = - childContextInitializers.entrySet().stream() - .map(e -> Map.entry(e.getKey(), (ApplicationContextInitializer) e.getValue())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return new BinderChildContextInitializer(downcastedInitializers); - } - - /** - * An AOT contribution that generates a application context initializers that can be used at runtime to populate - * the child binder contexts. - */ - private static class BinderChildContextAotContribution implements BeanRegistrationAotContribution { - - private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); - private final Map childContexts; - - BinderChildContextAotContribution(Map childContexts) { - this.childContexts = childContexts.entrySet().stream() - .filter(e -> GenericApplicationContext.class.isInstance(e.getValue())) - .map(e -> Map.entry(e.getKey(), GenericApplicationContext.class.cast(e.getValue()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - @Override - public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - ApplicationContextAotGenerator aotGenerator = new ApplicationContextAotGenerator(); - - GeneratedMethod postProcessorMethod = beanRegistrationCode.getMethods().add("addChildContextInitializers", - (method) -> { - method.addJavadoc("Use AOT child context initialization"); - method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); - method.addParameter(RegisteredBean.class, "registeredBean"); - method.addParameter(BinderChildContextInitializer.class, "instance"); - method.returns(BinderChildContextInitializer.class); - method.addStatement("$T> initializers = new $T<>()", Map.class, - ApplicationContextInitializer.class, ConfigurableApplicationContext.class, HashMap.class); - this.childContexts.forEach((name, context) -> { - this.logger.debug(() -> "Generating AOT child context initializer for " + name); - GenerationContext childGenerationContext = generationContext.withName(name + "Binder"); - ClassName initializerClassName = aotGenerator.processAheadOfTime(context, childGenerationContext); - method.addStatement("$T " + name + "Initializer = new $L()", ApplicationContextInitializer.class, - ConfigurableApplicationContext.class, initializerClassName); - method.addStatement("initializers.put($S," + name + "Initializer)", name); - }); - method.addStatement("return instance.withChildContextInitializers(initializers)"); - }); - beanRegistrationCode.addInstancePostProcessor(postProcessorMethod.toMethodReference()); - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderConfiguration.java deleted file mode 100644 index 4aa298cee..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderConfiguration.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.Map; -import java.util.Properties; - -/** - * Configuration for a binder instance, associating a {@link BinderType} with its - * configuration {@link Properties}. An application may contain multiple - * {@link BinderConfiguration}s per {@link BinderType}, when connecting to multiple - * systems of the same type. - * - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public class BinderConfiguration { - - private final String binderType; - - private final Map properties; - - private final boolean inheritEnvironment; - - private final boolean defaultCandidate; - - /** - * @param binderType the binder type used by this configuration - * @param properties the properties for setting up the binder - * @param inheritEnvironment whether the binder should inherit the environment of the - * application - * @param defaultCandidate whether the binder should be considered as a candidate when - * determining a default - */ - public BinderConfiguration(String binderType, Map properties, - boolean inheritEnvironment, boolean defaultCandidate) { - this.binderType = binderType; - this.properties = properties; - this.inheritEnvironment = inheritEnvironment; - this.defaultCandidate = defaultCandidate; - } - - public String getBinderType() { - return this.binderType; - } - - public Map getProperties() { - return this.properties; - } - - public boolean isInheritEnvironment() { - return this.inheritEnvironment; - } - - public boolean isDefaultCandidate() { - return this.defaultCandidate; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderCustomizer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderCustomizer.java deleted file mode 100644 index 5e15de867..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderCustomizer.java +++ /dev/null @@ -1,40 +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.cloud.stream.binder; - -/** - * @author Soby Chacko - * @since 3.1.0 - */ -public interface BinderCustomizer { - - /** - * When customization beans are present in an application that uses a single binder, - * those beans are detected by the binder. However, this is not the case in a multi-binder - * scenario, since various binders live in different application contexts. This customizer - * enables the application to properly apply customizations in all the binders. - * By providing an implementation of this interface, the binders, although reside - * in different application contexts, will receive the customization. - * Spring Cloud Stream ensures that the customizations take place before the binders are - * accessed. The user must check for the binder type and then apply the necessary customizations. - * In the case of a single binder, the use of this customizer is redundant. - * - * @param binder to be customized - * @param binderName binder name to distinguish between multiple instances of the same binder type - */ - void customize(Binder binder, String binderName); -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderErrorChannel.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderErrorChannel.java deleted file mode 100644 index 30fd487d3..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderErrorChannel.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.concurrent.atomic.AtomicInteger; - -import org.springframework.integration.channel.PublishSubscribeChannel; -import org.springframework.messaging.MessageHandler; - -/** - * A channel for errors. If it has a {@link LastSubscriberMessageHandler} subscriber, it - * can only have one and it will always be the last subscriber. - * - * @author Gary Russell - * @author Oleg Zhurakousky - * @author Byungjun You - * @since 1.3 - * - */ -class BinderErrorChannel extends PublishSubscribeChannel - implements LastSubscriberAwareChannel { - - private final AtomicInteger subscribers = new AtomicInteger(); - - private volatile LastSubscriberMessageHandler finalHandler; - - BinderErrorChannel() { - super(true); - } - - @Override - public boolean subscribe(MessageHandler handler) { - this.subscribers.incrementAndGet(); - if (handler instanceof LastSubscriberMessageHandler - && this.finalHandler != null) { - throw new IllegalStateException( - "Only one LastSubscriberMessageHandler is allowed"); - } - if (this.finalHandler != null) { - super.unsubscribe(this.finalHandler); - } - boolean result = super.subscribe(handler); - if (this.finalHandler != null) { - super.subscribe(this.finalHandler); - } - if (handler instanceof LastSubscriberMessageHandler lastSubscriberMessageHandler - && this.finalHandler == null) { - this.finalHandler = lastSubscriberMessageHandler; - } - return result; - } - - @Override - public boolean unsubscribe(MessageHandler handler) { - this.subscribers.decrementAndGet(); - return super.unsubscribe(handler); - } - - @Override - public int subscribers() { - return this.subscribers.get(); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderException.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderException.java deleted file mode 100644 index 894d8bbd7..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderException.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -/** - * Exception thrown to indicate a binder error (most likely a configuration error). - * - * @author Gary Russell - * @author Marius Bogoevici - */ -@SuppressWarnings("serial") -public class BinderException extends RuntimeException { - - public BinderException(String message) { - super(message); - } - - public BinderException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderFactory.java deleted file mode 100644 index fe16b24ca..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -/** - * @author Marius Bogoevici - */ -public interface BinderFactory { - - /** - * Returns the binder instance associated with the given configuration name. Instance - * caching is a requirement, and implementations must return the same instance on - * subsequent invocations with the same arguments. - * @param the primary binding type - * @param configurationName the name of a binder configuration - * @param bindableType binding target type - * @return the binder instance - */ - Binder getBinder( - String configurationName, Class bindableType); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderHeaders.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderHeaders.java deleted file mode 100644 index 515b4147a..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderHeaders.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2015-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.integration.IntegrationMessageHeaderAccessor; -import org.springframework.messaging.MessageHeaders; - -/** - * Spring Integration message headers for Spring Cloud Stream. - * - * @author Gary Russell - * @author David Turanski - * @author Soby Chacko - */ -public final class BinderHeaders { - - /** - * The headers that will be propagated, by default, by binder implementations that - * have no inherent header support (by embedding the headers in the payload). - */ - public static final String[] STANDARD_HEADERS = new String[] { - IntegrationMessageHeaderAccessor.CORRELATION_ID, - IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, - IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, MessageHeaders.CONTENT_TYPE}; - - private static final String PREFIX = "scst_"; - - /** - * Indicates the name of the target destination the binder should use if they - * choose (or have capabilities) to optimize sending output messages to - * dynamic destinations. - *
- *
- * NOTE: The core framework only defines this header, but does nothing else. - * So it is up to binders to choose to support it or not. One can always rely on {@link StreamBridge} - * for general cases of sending output messages to dynamic destinations. - */ - public static final String TARGET_DESTINATION = PREFIX + "targetDestination"; - - /** - * Indicates the target partition of an outbound message. Binders must observe this - * value when sending data on the transport. This header is internally set by the - * framework when partitioning is configured. It may be overridden by - * {@link BinderHeaders#PARTITION_OVERRIDE} if set by the user. - */ - public static final String PARTITION_HEADER = PREFIX + "partition"; - - /** - * Indicates the target partition of an outbound message. Overrides any partition - * selected by the binder. This header takes precedence over - * {@link BinderHeaders#PARTITION_HEADER}. - */ - public static final String PARTITION_OVERRIDE = PREFIX + "partitionOverride"; - - /** - * Indicates that an incoming message has native headers. - * @since 2.0 - */ - public static final String NATIVE_HEADERS_PRESENT = PREFIX + "nativeHeadersPresent"; - - /** - * Indicates the Spring Cloud Stream version. Used for determining if legacy content - * type check supported or not. - * @since 2.0 - */ - public static final String SCST_VERSION = PREFIX + "version"; - - private BinderHeaders() { - super(); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderSpecificPropertiesProvider.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderSpecificPropertiesProvider.java deleted file mode 100644 index 5c3b95c01..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderSpecificPropertiesProvider.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -/** - * @author Oleg Zhurakousky - * @since 2.1 - * - */ -public interface BinderSpecificPropertiesProvider { - - Object getConsumer(); - - Object getProducer(); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderType.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderType.java deleted file mode 100644 index 827f876cc..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderType.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import java.util.Arrays; - -/** - * References one or more - * {@link org.springframework.context.annotation.Configuration}-annotated classes which - * provide a context definition which contains exactly one {@link Binder}, typically for a - * given type of system (e.g. Rabbit, Kafka, Redis, etc.). An application may contain - * multiple instances of a given {@link BinderType}, when connecting to multiple systems - * of the same type. - * - * @author Marius Bogoevici - */ -public class BinderType { - - private final String defaultName; - - private final Class[] configurationClasses; - - public BinderType(String defaultName, Class[] configurationClasses) { - this.defaultName = defaultName; - this.configurationClasses = configurationClasses; - } - - public String getDefaultName() { - return this.defaultName; - } - - public Class[] getConfigurationClasses() { - return this.configurationClasses; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - BinderType that = (BinderType) o; - if (!this.defaultName.equals(that.defaultName)) { - return false; - } - return Arrays.equals(this.configurationClasses, that.configurationClasses); - - } - - @Override - public int hashCode() { - int result = this.defaultName.hashCode(); - result = 31 * result + Arrays.hashCode(this.configurationClasses); - return result; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderTypeRegistry.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderTypeRegistry.java deleted file mode 100644 index cca64c010..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderTypeRegistry.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.Map; - -/** - * A registry of {@link BinderType}s, indexed by name. A {@link BinderTypeRegistry} bean - * is created automatically based on information found in the - * {@literal META-INF/spring.binders} files provided by binder implementors. - * - * @author Marius Bogoevici - */ -public interface BinderTypeRegistry { - - BinderType get(String name); - - Map getAll(); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java deleted file mode 100644 index 1d678cbbb..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2016-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.Collections; -import java.util.Map; - -import org.springframework.cloud.stream.endpoint.BindingsEndpoint; -import org.springframework.integration.core.Pausable; - -/** - * Represents a binding between an input or output and an adapter endpoint that connects - * via a Binder. The binding could be for a consumer or a producer. A consumer binding - * represents a connection from an adapter to an input. A producer binding represents a - * connection from an output to an adapter. - * - * @param type of binding - * @author Jennifer Hickey - * @author Mark Fisher - * @author Gary Russell - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public interface Binding extends Pausable { - - default Map getExtendedInfo() { - return Collections.emptyMap(); - } - - /** - * Starts the target component represented by this instance. NOTE: At the time the - * instance is created the component is already started. This operation is typically - * used by actuator to re-bind/re-start. - * - * @see BindingsEndpoint - */ - @Override - default void start() { - } - - /** - * Stops the target component represented by this instance. NOTE: At the time the - * instance is created the component is already started. This operation is typically - * used by actuator to re-bind/re-start. - * - * @see BindingsEndpoint - */ - @Override - default void stop() { - } - - /** - * Will always return false unless overriden. - */ - @Override - default boolean isPaused() { - return false; - } - - /** - * Pauses the target component represented by this instance if and only if the - * component implements {@link Pausable} interface NOTE: At the time the instance is - * created the component is already started and active. This operation is typically - * used by actuator to pause/resume. - * - * @see BindingsEndpoint - */ - @Override - default void pause() { - this.stop(); - } - - /** - * Resumes the target component represented by this instance if and only if the - * component implements {@link Pausable} interface NOTE: At the time the instance is - * created the component is already started and active. This operation is typically - * used by actuator to pause/resume. - * - * @see BindingsEndpoint - */ - @Override - default void resume() { - this.start(); - } - - /** - * @return 'true' if the target component represented by this instance is running. - */ - @Override - default boolean isRunning() { - return false; - } - - /** - * Returns the name of the destination for this binding. - * @return destination name - */ - default String getName() { - return null; - } - - /** - * Returns the name of the target for this binding (i.e., channel name). - * @return binding name - * - * @since 2.2 - */ - default String getBindingName() { - return null; - } - - /** - * Returns the name of the binder for this binding. - * @return binder name - * - * @since 4.0.2 - */ - default String getBinderName() { - return null; - } - - /** - * Returns the type of the binder for this binding. - * @return binder name - * - * @since 4.0.2 - */ - default String getBinderType() { - return null; - } - - /** - * Unbinds the target component represented by this instance and stops any active - * components. Implementations must be idempotent. After this method is invoked, the - * target is not expected to receive any messages; this instance should be discarded, - * and a new Binding should be created instead. - */ - void unbind(); - - /** - * Returns boolean flag representing this binding's type. If 'true' this binding is an - * 'input' binding otherwise it is 'output' (as in binding annotated with - * either @Input or @Output). - * @return 'true' if this binding represents an input binding. - */ - default boolean isInput() { - throw new UnsupportedOperationException( - "Binding implementation `" + this.getClass().getName() - + "` must implement this operation before it is called"); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BindingCleaner.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BindingCleaner.java deleted file mode 100644 index f1b512c90..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BindingCleaner.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import java.util.List; -import java.util.Map; - -/** - * Interface for implementations that perform cleanup for binders. - * - * @author Gary Russell - * @since 1.2 - */ -public interface BindingCleaner { - - /** - * Clean up all resources for the supplied stream/job. - * @param entity the stream or job; may be terminated with a simple wild card '*', in - * which case all streams with names starting with the characters before the '*' will - * be cleaned. - * @param isJob true if the entity is a job. - * @return a map of lists of resources removed. - */ - Map> clean(String entity, boolean isJob); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BindingCreatedEvent.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BindingCreatedEvent.java deleted file mode 100644 index 9f9dcb855..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BindingCreatedEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import org.springframework.context.ApplicationEvent; - -/** - * ApplicationEvent fired whenever the the Binding is created. - * - * @author Oleg Zhurakousky - * @since 2.0 - * - * #see AbstractMessageChannelBinder - */ -@SuppressWarnings("serial") -public class BindingCreatedEvent extends ApplicationEvent { - - public BindingCreatedEvent(Binding source) { - super(source); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ConsumerProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ConsumerProperties.java deleted file mode 100644 index 2277ffb58..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ConsumerProperties.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import org.springframework.messaging.Message; - -/** - * Common consumer properties - spring.cloud.stream.bindings.[destinationName].consumer. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Soby Chacko - * @author Oleg Zhurakousky - * @author Nicolas Homble - * @author Michael Michailidis - */ -@JsonInclude(JsonInclude.Include.NON_DEFAULT) -public class ConsumerProperties { - - /** - * Binding name for this consumer binding. - */ - private String bindingName; - - /** - * Signals if this consumer needs to be started automatically. - * - * Default: true - */ - private boolean autoStartup = true; - - /** - * The concurrency setting of the consumer. Default: 1. - */ - private int concurrency = 1; - - /** - * Whether the consumer receives data from a partitioned producer. Default: 'false'. - */ - private boolean partitioned; - - /** - * When set to a value greater than equal to zero, allows customizing the instance - * count of this consumer (if different from spring.cloud.stream.instanceCount). When - * set to a negative value, it will default to spring.cloud.stream.instanceCount. See - * that property for more information. Default: -1 NOTE: This setting will override - * the one set in 'spring.cloud.stream.instance-count' - */ - private int instanceCount = -1; - - /** - * When set to a value greater than equal to zero, allows customizing the instance - * index of this consumer (if different from spring.cloud.stream.instanceIndex). When - * set to a negative value, it will default to spring.cloud.stream.instanceIndex. See - * that property for more information. Default: -1 NOTE: This setting will override - * the one set in 'spring.cloud.stream.instance-index' - */ - private int instanceIndex = -1; - - /** - * When set it will allow the customization of the consumer to spawn a consumer for - * each item in the list. All negative indexes will be discarded. Default: null - * NOTE: This setting will disable the instance-index - */ - private List instanceIndexList; - - /** - * The number of attempts to process the message (including the first) in the event of - * processing failures. This is a RetryTemplate configuration which is provided by the - * framework. Default: 3. Set to 1 to disable retry. You can also provide custom - * RetryTemplate in the event you want to take complete control of the RetryTemplate. - * Simply configure it as @Bean inside your application configuration. - */ - private int maxAttempts = 3; - - /** - * The backoff initial interval on retry. This is a RetryTemplate configuration which - * is provided by the framework. Default: 1000 ms. You can also provide custom - * RetryTemplate in the event you want to take complete control of the RetryTemplate. - * Simply configure it as @Bean inside your application configuration. - */ - private int backOffInitialInterval = 1000; - - /** - * The maximum backoff interval. This is a RetryTemplate configuration which is - * provided by the framework. Default: 10000 ms. You can also provide custom - * RetryTemplate in the event you want to take complete control of the RetryTemplate. - * Simply configure it as @Bean inside your application configuration. - */ - private int backOffMaxInterval = 10000; - - /** - * The backoff multiplier.This is a RetryTemplate configuration which is provided by - * the framework. Default: 2.0. You can also provide custom RetryTemplate in the event - * you want to take complete control of the RetryTemplate. Simply configure it - * as @Bean inside your application configuration. - */ - private double backOffMultiplier = 2.0; - - /** - * Whether exceptions thrown by the listener that are not listed in the - * 'retryableExceptions' are retryable. - */ - private boolean defaultRetryable = true; - - /** - * Allows you to further qualify which RetryTemplate to use for a specific consumer - * binding.. - */ - private String retryTemplateName; - - /** - * A map of Throwable class names in the key and a boolean in the value. Specify those - * exceptions (and subclasses) that will or won't be retried. - */ - private Map, Boolean> retryableExceptions = new LinkedHashMap<>(); - - /** - * When set to none, disables header parsing on input. Effective only for messaging - * middleware that does not support message headers natively and requires header - * embedding. This option is useful when consuming data from non-Spring Cloud Stream - * applications when native headers are not supported. When set to headers, uses the - * middleware’s native header mechanism. When set to 'embeddedHeaders', embeds headers - * into the message payload. Default: depends on binder implementation. Rabbit and - * Kafka binders currently distributed with spring cloud stream support headers - * natively. - */ - private HeaderMode headerMode; - - /** - * When set to true, the inbound message is deserialized directly by client library, - * which must be configured correspondingly (e.g. setting an appropriate Kafka - * producer value serializer). NOTE: This is binder specific setting which has no - * effect if binder does not support native serialization/deserialization. Currently - * only Kafka binder supports it. Default: 'false' - */ - private boolean useNativeDecoding; - - /** - * When set to true, the underlying binder will natively multiplex destinations on the - * same input binding. For example, in the case of a comma separated multiple - * destinations, the core framework will skip binding them individually if this is set - * to true, but delegate that responsibility to the binder. - * - * By default this property is set to `false` and the binder will individually bind - * each destinations in case of a comma separated multi destination list. The - * individual binder implementations that need to support multiple input bindings - * natively (multiplex) can enable this property. - */ - private boolean multiplex; - - /** - * When set to true, if the binder supports it, the messages emitted will have a {@link List} - * payload; When used in conjunction with functions, the function can receive a list of - * objects (or {@link Message}s) with the payloads converted if necessary. - * - * @since 3.0 - */ - private boolean batchMode; - - public String getBindingName() { - return bindingName; - } - - /** - * This method is not intended as a configuration property to set by the applications. - * Therefore, we are not providing a proper setter method for this. - * @param bindingName binding name populated by the framework. - */ - public void populateBindingName(String bindingName) { - this.bindingName = bindingName; - } - - public String getRetryTemplateName() { - return retryTemplateName; - } - - public void setRetryTemplateName(String retryTemplateName) { - this.retryTemplateName = retryTemplateName; - } - - public int getConcurrency() { - return this.concurrency; - } - - public void setConcurrency(int concurrency) { - this.concurrency = concurrency; - } - - public boolean isPartitioned() { - return this.partitioned; - } - - public void setPartitioned(boolean partitioned) { - this.partitioned = partitioned; - } - - public int getInstanceCount() { - return this.instanceCount; - } - - public void setInstanceCount(int instanceCount) { - this.instanceCount = instanceCount; - } - - public int getInstanceIndex() { - return this.instanceIndex; - } - - public void setInstanceIndex(int instanceIndex) { - this.instanceIndex = instanceIndex; - } - - public List getInstanceIndexList() { - return this.instanceIndexList; - } - - public void setInstanceIndexList(List instanceIndexList) { - this.instanceIndexList = instanceIndexList; - } - - public int getMaxAttempts() { - return this.maxAttempts; - } - - public void setMaxAttempts(int maxAttempts) { - this.maxAttempts = maxAttempts; - } - - public int getBackOffInitialInterval() { - return this.backOffInitialInterval; - } - - public void setBackOffInitialInterval(int backOffInitialInterval) { - this.backOffInitialInterval = backOffInitialInterval; - } - - public int getBackOffMaxInterval() { - return this.backOffMaxInterval; - } - - public void setBackOffMaxInterval(int backOffMaxInterval) { - this.backOffMaxInterval = backOffMaxInterval; - } - - public double getBackOffMultiplier() { - return this.backOffMultiplier; - } - - public void setBackOffMultiplier(double backOffMultiplier) { - this.backOffMultiplier = backOffMultiplier; - } - - public boolean isDefaultRetryable() { - return this.defaultRetryable; - } - - public void setDefaultRetryable(boolean defaultRetryable) { - this.defaultRetryable = defaultRetryable; - } - - public Map, Boolean> getRetryableExceptions() { - return this.retryableExceptions; - } - - public void setRetryableExceptions( - Map, Boolean> retryableExceptions) { - this.retryableExceptions = retryableExceptions; - } - - public HeaderMode getHeaderMode() { - return this.headerMode; - } - - public void setHeaderMode(HeaderMode headerMode) { - this.headerMode = headerMode; - } - - public boolean isUseNativeDecoding() { - return this.useNativeDecoding; - } - - public void setUseNativeDecoding(boolean useNativeDecoding) { - this.useNativeDecoding = useNativeDecoding; - } - - public boolean isMultiplex() { - return this.multiplex; - } - - public void setMultiplex(boolean multiplex) { - this.multiplex = multiplex; - } - - public boolean isAutoStartup() { - return this.autoStartup; - } - - public void setAutoStartup(boolean autoStartup) { - this.autoStartup = autoStartup; - } - - public boolean isBatchMode() { - return this.batchMode; - } - - public void setBatchMode(boolean batchMode) { - this.batchMode = batchMode; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java deleted file mode 100644 index db3a5e6fc..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderFactory.java +++ /dev/null @@ -1,647 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.net.URL; -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.Set; -import java.util.stream.Stream; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; -import org.springframework.cloud.stream.reflection.GenericsUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.io.UrlResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * Default {@link BinderFactory} implementation. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Oleg Zhurakousky - * @author Soby Chacko - * @author Artem Bilan - * @author Anshul Mehra - * @author Chris Bono - * @author Byungjun You - * @author Omer Celik - */ -public class DefaultBinderFactory implements BinderFactory, DisposableBean, ApplicationContextAware { - - protected final Log logger = LogFactory.getLog(getClass()); - - private final Map binderConfigurations; - - private final Map, ConfigurableApplicationContext>> binderInstanceCache = new HashMap<>(); - - private final Map defaultBinderForBindingTargetType = new HashMap<>(); - - private final Map> binderChildContextInitializers = new HashMap<>(); - - private final BinderTypeRegistry binderTypeRegistry; - - private final BinderCustomizer binderCustomizer; - - private volatile ConfigurableApplicationContext context; - - private Collection listeners; - - private volatile String defaultBinder; - - public DefaultBinderFactory(Map binderConfigurations, - BinderTypeRegistry binderTypeRegistry, BinderCustomizer binderCustomizer) { - this.binderConfigurations = new HashMap<>(binderConfigurations); - this.binderTypeRegistry = binderTypeRegistry; - this.binderCustomizer = binderCustomizer; - } - - /** - * Replaces the existing binder configurations - useful in AOT processing where the binding service properties - * have to be manually loaded after the binder factory is constructed. - * - * @param binderConfigurations the updated configurations - */ - void updateBinderConfigurations(Map binderConfigurations) { - this.binderConfigurations.clear(); - this.binderConfigurations.putAll(binderConfigurations); - } - - BinderTypeRegistry getBinderTypeRegistry() { - return this.binderTypeRegistry; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext); - this.context = (ConfigurableApplicationContext) applicationContext; - } - - public void setDefaultBinder(String defaultBinder) { - this.defaultBinder = defaultBinder; - } - - public void setListeners(Collection listeners) { - this.listeners = listeners; - } - - @Override - public void destroy() { - this.binderInstanceCache.values().stream().map(Entry::getValue).forEach(ConfigurableApplicationContext::close); - this.defaultBinderForBindingTargetType.clear(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - - public synchronized Binder getBinder(String name, Class bindingTargetType) { - String binderName = StringUtils.hasText(name) ? name : this.defaultBinder; - - Map binders = this.context == null ? Collections.emptyMap() : this.context.getBeansOfType(Binder.class); - Binder binder; - if (StringUtils.hasText(binderName) && binders.containsKey(binderName)) { - binder = (Binder) this.context.getBean(binderName); - } - else if (binders.size() == 1) { - binder = binders.values().iterator().next(); - } - else if (binders.size() > 1) { - throw new IllegalStateException( - "Multiple binders are available, however neither default nor " - + "per-destination binder name is provided. Available binders are " - + binders.keySet()); - } - else { - /* - * This is the fallback to the old bootstrap that relies on spring.binders. - */ - binder = this.doGetBinder(binderName, bindingTargetType); - } - if (this.binderCustomizer != null) { - this.binderCustomizer.customize(binder, binderName); - } - return binder; - } - - private Binder doGetBinder(String name, Class bindingTargetType) { - // If child initializers - use AOT lookup - if (!CollectionUtils.isEmpty(this.binderChildContextInitializers)) { - return this.doGetBinderAOT(name, bindingTargetType); - } - return this.doGetBinderConventional(name, bindingTargetType); - } - - private Binder doGetBinderAOT(String name, Class bindingTargetType) { - // If neither name nor default given - return single or fail when > 1 - if (!StringUtils.hasText(name) && !StringUtils.hasText(this.defaultBinder)) { - if (this.binderChildContextInitializers.size() == 1) { - String configurationName = this.binderChildContextInitializers.keySet().iterator().next(); - this.logger.debug("No specific name or default given - using single available child initializer '" + configurationName + "'"); - return this.getBinderInstance(configurationName); - } - throw new IllegalStateException("No specific name or default given - can't determine which binder to use"); - } - - // Prefer specific name over default - String configurationName = name; - if (!StringUtils.hasText(configurationName)) { - configurationName = this.defaultBinder; - } - - // Check for matching child initializer - if (this.binderChildContextInitializers.containsKey(configurationName)) { - return this.getBinderInstance(configurationName); - } - - throw new IllegalStateException("Requested binder '" + name + "' did not match available binders: " + - this.binderChildContextInitializers.keySet()); - } - - private Binder doGetBinderConventional(String name, - Class bindingTargetType) { - - if (!MessageChannel.class.isAssignableFrom(bindingTargetType) - && !PollableMessageSource.class.isAssignableFrom(bindingTargetType)) { - String bindingTargetTypeName = StringUtils.hasText(name) ? name : bindingTargetType.getSimpleName().toLowerCase(); - Binder binderInstance = getBinderInstance(bindingTargetTypeName); - return binderInstance; - } - String configurationName; - // Fall back to a default if no argument is provided - if (!StringUtils.hasText(name)) { - Assert.notEmpty(this.binderConfigurations, - "A default binder has been requested, but there is no binder available "); - if (!StringUtils.hasText(this.defaultBinder)) { - Set defaultCandidateConfigurations = new HashSet<>(); - for (Map.Entry binderConfigurationEntry : this.binderConfigurations - .entrySet()) { - if ("integration".equals(binderConfigurationEntry.getKey())) { - defaultCandidateConfigurations.add(binderConfigurationEntry.getKey()); - break; - } - else if (binderConfigurationEntry.getValue().isDefaultCandidate()) { - defaultCandidateConfigurations - .add(binderConfigurationEntry.getKey()); - } - } - if (defaultCandidateConfigurations.size() == 1) { - configurationName = defaultCandidateConfigurations.iterator().next(); - this.defaultBinderForBindingTargetType - .put(bindingTargetType.getName(), configurationName); - } - else { - List candidatesForBindableType = new ArrayList<>(); - for (String defaultCandidateConfiguration : defaultCandidateConfigurations) { - Binder binderInstance = getBinderInstance(defaultCandidateConfiguration); - - Class binderType = GenericsUtils.getParameterType( - binderInstance.getClass(), Binder.class, 0); - if (binderType.isAssignableFrom(bindingTargetType)) { - populateCandidatesForBindableType(bindingTargetType, candidatesForBindableType, defaultCandidateConfiguration); - } - } - if (candidatesForBindableType.size() == 1) { - configurationName = candidatesForBindableType.iterator().next(); - this.defaultBinderForBindingTargetType - .put(bindingTargetType.getName(), configurationName); - } - else { - String countMsg = (candidatesForBindableType.size() == 0) - ? "are no binders" : "is more than one binder"; - throw new IllegalStateException( - "A default binder has been requested, but there " - + countMsg + " available for '" - + bindingTargetType.getName() + "' : " - + StringUtils.collectionToCommaDelimitedString( - candidatesForBindableType) - + ", and no default binder has been set."); - } - } - } - else { - configurationName = this.defaultBinder; - } - } - else { - configurationName = name; - } - Binder binderInstance = getBinderInstance(configurationName); - Assert.state(verifyBinderTypeMatchesTarget(binderInstance, bindingTargetType), - "The binder '" + configurationName + "' cannot bind a " - + bindingTargetType.getName()); - return binderInstance; - } - - private void populateCandidatesForBindableType(Class bindingTargetType, List candidatesForBindableType, - String defaultCandidateConfiguration) { - // Going by the convention of proper reactor based binders start with the key literal - reactor - boolean isCandidate = (FluxMessageChannel.class.isAssignableFrom(bindingTargetType) && defaultCandidateConfiguration.startsWith("reactor")) - || !defaultCandidateConfiguration.startsWith("reactor"); - if (isCandidate) { - candidatesForBindableType.add(defaultCandidateConfiguration); - } - } - - /** - * Return true if the binder is a {@link PollableConsumerBinder} and the target type - * is a {@link PollableSource} and their generic types match (e.g. MessageHandler), OR - * if it's a {@link Binder} and the target matches the binder's generic type. - * @param bindng target type - * @param binderInstance the binder. - * @param bindingTargetType the binding target type. - * @return true if the conditions match. - */ - private boolean verifyBinderTypeMatchesTarget(Binder binderInstance, - Class bindingTargetType) { - return (binderInstance instanceof PollableConsumerBinder && GenericsUtils - .checkCompatiblePollableBinder(binderInstance, bindingTargetType)) - || GenericsUtils - .getParameterType(binderInstance.getClass(), Binder.class, 0) - .isAssignableFrom(bindingTargetType); - } - - @SuppressWarnings("unchecked") - private Binder getBinderInstance(String configurationName) { - if (!this.binderInstanceCache.containsKey(configurationName)) { - this.logger.info("Creating binder: " + configurationName); - BinderConfiguration binderConfiguration = this.binderConfigurations.get(configurationName); - Map binderProperties = new HashMap<>(); - - ConfigurableApplicationContext binderProducingContext; - if (this.binderChildContextInitializers.containsKey(configurationName)) { - this.logger.info("Using AOT pre-prepared initializer to construct binder child context for " + configurationName); - if (binderConfiguration != null) { - this.flatten(null, binderConfiguration.getProperties(), binderProperties); - } - binderProducingContext = this.createUnitializedContextForAOT(configurationName, binderProperties, binderConfiguration); - this.binderChildContextInitializers.get(configurationName).initialize(binderProducingContext); - registerOuterContextBean(binderProperties, binderProducingContext); - binderProducingContext.refresh(); - } - else { - this.logger.info("Constructing binder child context for " + configurationName); - Assert.state(binderConfiguration != null, "Unknown binder configuration: " + configurationName); - this.flatten(null, binderConfiguration.getProperties(), binderProperties); - BinderType binderType = this.binderTypeRegistry.get(binderConfiguration.getBinderType()); - Assert.notNull(binderType, "Binder type " + binderConfiguration.getBinderType() + " is not defined"); - binderProducingContext = this.initializeBinderContextSimple(configurationName, binderProperties, - binderType, binderConfiguration, true); - } - - Map messageConverters = binderProducingContext.getBeansOfType(MessageConverter.class); - if (!CollectionUtils.isEmpty(messageConverters) && !ObjectUtils.isEmpty(context.getBeansOfType(FunctionCatalog.class))) { - FunctionCatalog functionCatalog = this.context.getBean(FunctionCatalog.class); - if (functionCatalog instanceof SimpleFunctionRegistry simpleFunctionRegistry) { - simpleFunctionRegistry.addMessageConverters(messageConverters.values()); - } - } - - Binder binder = binderProducingContext.getBean(Binder.class); - /* - * This will ensure that application defined errorChannel and other beans are - * accessible within binder's context (see - * https://github.com/spring-cloud/spring-cloud-stream/issues/1384) - */ - if (this.context != null && binder instanceof ApplicationContextAware applicationContextAwareBinder) { - applicationContextAwareBinder.setApplicationContext(this.context); - } - if (!CollectionUtils.isEmpty(this.listeners)) { - for (Listener binderFactoryListener : this.listeners) { - binderFactoryListener.afterBinderContextInitialized(configurationName, - binderProducingContext); - } - } - logger.info("Caching the binder: " + configurationName); - this.binderInstanceCache.put(configurationName, - new SimpleImmutableEntry<>(binder, binderProducingContext)); - } - logger.trace("Retrieving cached binder: " + configurationName); - return (Binder) this.binderInstanceCache - .get(configurationName).getKey(); - } - - - /** - * @return map of binder name to binder configuration - */ - Map getBinderConfigurations() { - return this.binderConfigurations; - } - - /** - * Creates a binder child application context that can be used by AOT for pre-generation. - * @param configurationName binder configuration name - * @return binder child application context that has not been refreshed - */ - ConfigurableApplicationContext createBinderContextForAOT(String configurationName) { - logger.info("Pre-creating binder child context (AOT) for " + configurationName); - BinderConfiguration binderConfiguration = this.binderConfigurations.get(configurationName); - Assert.state(binderConfiguration != null, "Unknown binder configuration: " + configurationName); - BinderType binderType = this.binderTypeRegistry.get(binderConfiguration.getBinderType()); - Assert.notNull(binderType, "Binder type " + binderConfiguration.getBinderType() + " is not defined"); - Map binderProperties = new HashMap<>(); - this.flatten(null, binderConfiguration.getProperties(), binderProperties); - return initializeBinderContextSimple(configurationName, binderProperties, binderType, binderConfiguration, false); - } - - /** - * Sets the initializers to use when populating a binder child application context. - *

- * This is useful for the AOT scenario where the child binder contexts have been pre-generated into the form of an - * application context initializer. - * - * @param binderChildContextInitializers map of binder configuration name to initializer for the binder child context - */ - void setBinderChildContextInitializers(Map> binderChildContextInitializers) { - this.binderChildContextInitializers.clear(); - this.binderChildContextInitializers.putAll(binderChildContextInitializers); - } - - /** - * Creates and optionally refreshes a binder child application context. - * @param configurationName binder configuration name - * @param binderProperties binder properties - * @param binderType binder type - * @param binderConfiguration binder configuration - * @param refresh whether to refresh the context - * @return refreshed binder child application context - */ - @SuppressWarnings("rawtypes") - ConfigurableApplicationContext initializeBinderContextSimple(String configurationName, Map binderProperties, - BinderType binderType, BinderConfiguration binderConfiguration, boolean refresh) { - AnnotationConfigApplicationContext binderProducingContext = new AnnotationConfigApplicationContext(); - if (this.context != null) { - binderProducingContext.getBeanFactory().setConversionService(this.context.getBeanFactory().getConversionService()); - } - - List sourceClasses = new ArrayList<>(); - sourceClasses.addAll(Arrays.asList(binderType.getConfigurationClasses())); - if (binderProperties.containsKey("spring.main.sources")) { - String sources = (String) binderProperties.get("spring.main.sources"); - if (StringUtils.hasText(sources)) { - Stream.of(sources.split(",")).forEach(source -> { - try { - sourceClasses.add(Thread.currentThread().getContextClassLoader().loadClass(source.trim())); - } - catch (Exception e) { - throw new IllegalStateException("Failed to load class " + source, e); - } - }); - } - } - - binderProducingContext.register(sourceClasses.toArray(new Class[] {})); - MapPropertySource binderPropertySource = new MapPropertySource(configurationName, binderProperties); - binderProducingContext.getEnvironment().getPropertySources().addFirst(binderPropertySource); - binderProducingContext.setDisplayName(configurationName + "_context"); - boolean useApplicationContextAsParent = binderProperties.isEmpty() - && this.context != null; - ConfigurableEnvironment environment = this.context != null - ? this.context.getEnvironment() : null; - if (useApplicationContextAsParent) { - binderProducingContext.setParent(this.context); - } - else if (this.context != null) { - binderProducingContext.addApplicationListener(new ApplicationListener() { - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (context != null) { - try { - context.publishEvent(event); - } - catch (Exception e) { - logger.warn("Failed to publish " + event, e); - } - } - } - }); - - if (environment != null && !useApplicationContextAsParent) { - InitializerWithOuterContext initializer = new InitializerWithOuterContext(this.context); - initializer.initialize(binderProducingContext); - } - - if (environment != null && (useApplicationContextAsParent - || binderConfiguration.isInheritEnvironment())) { - binderProducingContext.getEnvironment().merge(environment); - binderProducingContext.getEnvironment().getPropertySources().remove("configurationProperties"); - binderProducingContext.getEnvironment().getPropertySources() - .addFirst(new MapPropertySource("defaultBinderFactoryProperties", - Collections.singletonMap("spring.main.web-application-type", "NONE"))); - } - } - - if (refresh) { - binderProducingContext.refresh(); - if (!useApplicationContextAsParent || "integration".equals(binderType.getDefaultName())) { - this.propagateSharedBeans((GenericApplicationContext) this.context, binderProducingContext); - } - } - - return binderProducingContext; - } - - private void propagateSharedBeans(GenericApplicationContext toContext, GenericApplicationContext fromContext) { - if (toContext == null) { - return; - } - GenericConversionService binderProducingConversionService = (GenericConversionService) toContext.getBeanFactory().getConversionService(); - try { - Enumeration resources = ClassUtils.getDefaultClassLoader().getResources("META-INF/shared.beans"); - while (resources.hasMoreElements()) { - URL url = resources.nextElement(); - UrlResource resource = new UrlResource(url); - Properties properties = PropertiesLoaderUtils.loadProperties(resource); - Set classNames = properties.keySet(); - for (Object className : classNames) { - Class beanType = this.loadClass(((String) className).trim()); - if (beanType != null) { - Map beansOfType = fromContext.getBeansOfType(beanType); - beansOfType.entrySet().stream().forEach(entry -> { - Object bean = entry.getValue(); - if (bean instanceof Converter) { - binderProducingConversionService.addConverter((Converter) bean); - } - else { - toContext.registerBean(entry.getKey() + "_child", beanType, () -> entry.getValue()); - } - }); - } - } - } - } - catch (Exception e) { - logger.warn("Failed to propagate child beans. This may cause issues in your application", e); - } - } - - @SuppressWarnings("unchecked") - private Class loadClass(String className) { - try { - return (Class) ClassUtils.getDefaultClassLoader().loadClass(className.trim()); - } - catch (Throwable e) { - logger.debug("Attempt to load " + className + " failed.", e); - return null; - } - } - - /** - * Creates a bare minimum application context that can be initialized by AOT. - * - * @param configurationName binder configuration name - * @param binderProperties binder properties - * @param binderConfiguration binder configuration - * @return a binder child application context suitable for AOT initialization - */ - GenericApplicationContext createUnitializedContextForAOT(String configurationName, - Map binderProperties, BinderConfiguration binderConfiguration) { - GenericApplicationContext binderContext = new GenericApplicationContext(); - // Set the conversion service on the binder producing context to handle complex properties - if (this.context != null) { - binderContext.getBeanFactory().setConversionService(this.context.getBeanFactory().getConversionService()); - } - MapPropertySource binderPropertySource = new MapPropertySource(configurationName, binderProperties); - binderContext.getEnvironment().getPropertySources().addFirst(binderPropertySource); - binderContext.setDisplayName(configurationName + "_context"); - boolean useApplicationContextAsParent = binderProperties.isEmpty() && this.context != null; - ConfigurableEnvironment environment = this.context != null ? this.context.getEnvironment() : null; - if (useApplicationContextAsParent) { - binderContext.setParent(this.context); - } - else if (this.context != null) { - binderContext.addApplicationListener(event -> { - if (context != null) { - try { - context.publishEvent(event); - } - catch (Exception e) { - logger.warn("Failed to publish " + event, e); - } - } - }); - if (environment != null && (useApplicationContextAsParent || binderConfiguration.isInheritEnvironment())) { - binderContext.getEnvironment().merge(environment); - binderContext.getEnvironment().getPropertySources().remove("configurationProperties"); - binderContext.getEnvironment().getPropertySources() - .addFirst(new MapPropertySource("defaultBinderFactoryProperties", - Collections.singletonMap("spring.main.web-application-type", "NONE"))); - } - } - return binderContext; - } - - private void registerOuterContextBean(Map binderProperties, ConfigurableApplicationContext binderProducingContext) { - ConfigurableEnvironment environment = this.context != null - ? this.context.getEnvironment() : null; - boolean useApplicationContextAsParent = binderProperties.isEmpty() - && this.context != null; - if (environment != null && !useApplicationContextAsParent) { - InitializerWithOuterContext initializer = new InitializerWithOuterContext(this.context); - initializer.initialize(binderProducingContext); - } - } - - /** - * Ensures that nested properties are flattened (i.e., foo.bar=baz instead of - * foo={bar=baz}). - * @param propertyName property name to flatten - * @param value value that contains the property name - * @param flattenedProperties map to which we'll add the flattened property - */ - @SuppressWarnings("unchecked") - private void flatten(String propertyName, Object value, Map flattenedProperties) { - if (value instanceof Map valueAsMap) { - valueAsMap.forEach((k, v) -> flatten( - (propertyName != null ? propertyName + "." : "") + k, v, flattenedProperties)); - } - else { - flattenedProperties.put(propertyName, value.toString()); - } - } - - /** - * A listener that can be registered with the {@link DefaultBinderFactory} that allows - * the registration of additional configuration. - * - * @author Ilayaperumal Gopinathan - */ - public interface Listener { - - /** - * Applying additional capabilities to the binder after the binder context has - * been initialized. - * @param configurationName the binder configuration name - * @param binderContext the application context of the binder - */ - void afterBinderContextInitialized(String configurationName, - ConfigurableApplicationContext binderContext); - - } - - private static class InitializerWithOuterContext - implements ApplicationContextInitializer { - - private final ApplicationContext context; - - InitializerWithOuterContext(ApplicationContext context) { - this.context = context; - } - - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - applicationContext.getBeanFactory().registerSingleton("outerContext", - this.context); - } - - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderTypeRegistry.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderTypeRegistry.java deleted file mode 100644 index c4055621e..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinderTypeRegistry.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Default implementation of a {@link BinderTypeRegistry}. - * - * @author Marius Bogoevici - */ -public class DefaultBinderTypeRegistry implements BinderTypeRegistry { - - private final Map binderTypes; - - public DefaultBinderTypeRegistry(Map binderTypes) { - this.binderTypes = Collections.unmodifiableMap(new HashMap<>(binderTypes)); - } - - @Override - public BinderType get(String name) { - return this.binderTypes.get(name); - } - - @Override - public Map getAll() { - return this.binderTypes; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinding.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinding.java deleted file mode 100644 index c9e382ddc..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultBinding.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.context.Lifecycle; -import org.springframework.integration.context.IntegrationObjectSupport; -import org.springframework.integration.core.Pausable; -import org.springframework.integration.support.context.NamedComponent; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * Default implementation for a {@link Binding}. - * - * @param type of binding - * @author Jennifer Hickey - * @author Mark Fisher - * @author Gary Russell - * @author Marius Bogoevici - * @author Oleg Zhurakousky - * @author Myeonghyeon Lee - * @author Soby Chacko - * @author Byungjun You - * @see org.springframework.cloud.stream.annotation.EnableBinding - */ - -@JsonPropertyOrder({ "bindingName", "name", "group", "pausable", "state" }) -@JsonIgnoreProperties("running") -public class DefaultBinding implements Binding { - - protected final String name; - - protected final String group; - - protected final T target; - - protected final Lifecycle lifecycle; - - private final Log logger = LogFactory.getLog(this.getClass().getName()); - - private boolean paused; - - private boolean restartable; - - private Lifecycle companion; - - /** - * Creates an instance that associates a given name, group and binding target with an - * optional {@link Lifecycle} component, which will be stopped during unbinding. - * @param name the name of the binding target - * @param group the group (only for input targets) - * @param target the binding target - * @param lifecycle {@link Lifecycle} that runs while the binding is active and will - * be stopped during unbinding - */ - public DefaultBinding(String name, String group, T target, Lifecycle lifecycle) { - Assert.notNull(target, "target must not be null"); - this.name = name; - this.group = group; - this.target = target; - this.lifecycle = lifecycle; - this.restartable = StringUtils.hasText(group); - } - - public DefaultBinding(String name, T target, Lifecycle lifecycle) { - this(name, null, target, lifecycle); - this.restartable = true; - } - - @Override - public String getName() { - return this.name; - } - - @Override - public String getBindingName() { - String resolvedName = (this.target instanceof IntegrationObjectSupport integrationObjectSupportTarget) - ? integrationObjectSupportTarget.getComponentName() : getName(); - return resolvedName == null ? getName() : resolvedName; - } - - public String getGroup() { - return this.group; - } - - public String getState() { - String state = "N/A"; - if (this.lifecycle != null) { - if (isPausable()) { - state = this.paused ? "paused" : this.getRunningState(); - } - else { - state = this.getRunningState(); - } - } - return state; - } - - @Override - public boolean isRunning() { - return this.lifecycle != null && this.lifecycle.isRunning(); - } - - public boolean isPausable() { - return this.lifecycle instanceof Pausable; - } - - @Override - public boolean isPaused() { - return this.paused; - } - - @Override - public synchronized void start() { - if (this.companion != null) { - this.companion.start(); - } - if (!this.isRunning()) { - if (this.lifecycle != null && this.restartable) { - this.lifecycle.start(); - } - else { - this.logger.warn("Can not re-bind an anonymous binding"); - } - } - } - - @Override - public synchronized void stop() { - if (this.companion != null) { - this.companion.stop(); - } - if (this.isRunning()) { - this.lifecycle.stop(); - } - } - - @Override - public synchronized void pause() { - if (this.lifecycle instanceof Pausable pausableLifecycle) { - pausableLifecycle.pause(); - this.paused = true; - } - else { - this.logger - .warn("Attempted to pause a component that does not support Pausable " - + this.lifecycle); - } - } - - @Override - public synchronized void resume() { - if (this.lifecycle instanceof Pausable pausableLifecycle) { - pausableLifecycle.resume(); - this.paused = false; - } - else { - this.logger.warn( - "Attempted to resume a component that does not support Pausable " - + this.lifecycle); - } - } - - @Override - public void unbind() { - this.stop(); - afterUnbind(); - } - - Lifecycle getEndpoint() { - return this.lifecycle; - } - - @Override - public String toString() { - return " Binding [name=" + this.name + ", target=" + this.target + ", lifecycle=" - + ((this.lifecycle instanceof NamedComponent namedComponentWithLifeCycle) - ? namedComponentWithLifeCycle.getComponentName() - : ObjectUtils.nullSafeToString(this.lifecycle)) - + "]"; - } - - /** - * Listener method that executes after unbinding. Subclasses can implement their own - * behaviour on unbinding by overriding this method. - */ - protected void afterUnbind() { - } - - private String getRunningState() { - return isRunning() ? "running" : "stopped"; - } - - /** - * Sets the companion Lifecycle. - * In most cases, when dealing with message producer (e.g., Supplier), performing - * any lifecycle operation on it does nothing as we may need to also perform the same operation on its companion - * object (e.g., SourcePollingChannelAdapter) - * - * @param companion instance of companion {@link Lifecycle} object - */ - public void setCompanion(Lifecycle companion) { - this.companion = companion; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultPollableMessageSource.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultPollableMessageSource.java deleted file mode 100644 index b6895bc08..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DefaultPollableMessageSource.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiConsumer; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; -import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.context.Lifecycle; -import org.springframework.core.AttributeAccessor; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.integration.StaticMessageHeaderAccessor; -import org.springframework.integration.acks.AckUtils; -import org.springframework.integration.acks.AcknowledgmentCallback; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.core.MessageSource; -import org.springframework.integration.core.MessagingTemplate; -import org.springframework.integration.support.DefaultErrorMessageStrategy; -import org.springframework.integration.support.ErrorMessageStrategy; -import org.springframework.integration.support.ErrorMessageUtils; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessageHandlingException; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.converter.MessageConversionException; -import org.springframework.messaging.converter.SmartMessageConverter; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.retry.RecoveryCallback; -import org.springframework.retry.RetryCallback; -import org.springframework.retry.RetryContext; -import org.springframework.retry.RetryListener; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; - -/** - * The default implementation of a {@link PollableMessageSource}. - * - * @author Gary Russell - * @author Oleg Zhurakousky - * @author David Turanski - * @author Byungjun You - * @since 2.0 - * - */ -public class DefaultPollableMessageSource - implements PollableMessageSource, Lifecycle, RetryListener { - - private static final Log log = LogFactory.getLog(DefaultPollableMessageSource.class); - - protected static final ThreadLocal attributesHolder = new ThreadLocal(); - - private static final DirectChannel dummyChannel = new DirectChannel(); - - static { - dummyChannel.setBeanName("dummy.required.by.nonnull.api"); - } - - private final List interceptors = new ArrayList<>(); - - private final MessagingTemplate messagingTemplate = new MessagingTemplate(); - - private final SmartMessageConverter messageConverter; - - private MessageSource source; - - private RetryTemplate retryTemplate; - - private RecoveryCallback recoveryCallback; - - private MessageChannel errorChannel; - - private ErrorMessageStrategy errorMessageStrategy = new DefaultErrorMessageStrategy(); - - private BiConsumer> attributesProvider; - - private boolean running; - - /** - * @param messageConverter instance of {@link SmartMessageConverter}. Can be null. - */ - public DefaultPollableMessageSource( - @Nullable SmartMessageConverter messageConverter) { - this.messageConverter = messageConverter; - } - - public void setSource(MessageSource source) { - ProxyFactory pf = new ProxyFactory(source); - class ReceiveAdvice implements MethodInterceptor { - - private final List interceptors = new ArrayList<>(); - - @Override - public Object invoke(MethodInvocation invocation) throws Throwable { - Object result = invocation.proceed(); - if (result instanceof Message received) { - for (ChannelInterceptor interceptor : this.interceptors) { - received = interceptor.preSend(received, dummyChannel); - if (received == null) { - return null; - } - } - return received; - } - return result; - } - - } - final ReceiveAdvice advice = new ReceiveAdvice(); - advice.interceptors.addAll(this.interceptors); - NameMatchMethodPointcutAdvisor sourceAdvisor = new NameMatchMethodPointcutAdvisor( - advice); - sourceAdvisor.addMethodName("receive"); - pf.addAdvisor(sourceAdvisor); - this.source = (MessageSource) pf.getProxy(); - } - - public void setRetryTemplate(RetryTemplate retryTemplate) { - retryTemplate.registerListener(this); - this.retryTemplate = retryTemplate; - } - - public void setRecoveryCallback(RecoveryCallback recoveryCallback) { - this.recoveryCallback = context -> { - if (!shouldRequeue((MessagingException) context.getLastThrowable())) { - return recoveryCallback.recover(context); - } - throw (MessagingException) context.getLastThrowable(); - }; - } - - public void setErrorChannel(MessageChannel errorChannel) { - this.errorChannel = errorChannel; - } - - public void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) { - Assert.notNull(errorMessageStrategy, "'errorMessageStrategy' cannot be null"); - this.errorMessageStrategy = errorMessageStrategy; - } - - public void setAttributesProvider( - BiConsumer> attributesProvider) { - this.attributesProvider = attributesProvider; - } - - public void addInterceptor(ChannelInterceptor interceptor) { - this.interceptors.add(interceptor); - } - - public void addInterceptor(int index, ChannelInterceptor interceptor) { - this.interceptors.add(index, interceptor); - } - - @Override - public synchronized boolean isRunning() { - return this.running; - } - - @Override - public synchronized void start() { - if (!this.running && this.source instanceof Lifecycle sourceWithLifecycle) { - sourceWithLifecycle.start(); - } - this.running = true; - } - - @Override - public synchronized void stop() { - if (this.running && this.source instanceof Lifecycle sourceWithLifeCycle) { - sourceWithLifeCycle.stop(); - } - this.running = false; - } - - @Override - public boolean poll(MessageHandler handler) { - return poll(handler, null); - } - - @Override - public boolean poll(MessageHandler handler, ParameterizedTypeReference type) { - Message message = this.receive(type); - if (message == null) { - return false; - } - - AcknowledgmentCallback ackCallback = StaticMessageHeaderAccessor - .getAcknowledgmentCallback(message); - - if (ackCallback == null) { - ackCallback = status -> log.warn("No AcknowledgementCallback defined. Status: " + status.name() + " " + message); - } - - try { - if (this.retryTemplate == null) { - this.handle(message, handler); - } - else { - this.retryTemplate.execute(context -> { - this.handle(message, handler); - return null; - }, this.recoveryCallback); - } - return true; - } - catch (MessagingException e) { - if (this.retryTemplate == null && !shouldRequeue(e)) { - try { - this.messagingTemplate.send(this.errorChannel, this.errorMessageStrategy - .buildErrorMessage(e, attributesHolder.get())); - } - catch (MessagingException e1) { - requeueOrNack(message, ackCallback, e1); - } - return true; - } - else { - requeueOrNack(message, ackCallback, e); - return true; - } - } - catch (Exception e) { - AckUtils.autoNack(ackCallback); - if (e instanceof MessageHandlingException messageHandlingException && - messageHandlingException.getFailedMessage().equals(message)) { - throw (MessageHandlingException) e; - } - throw new MessageHandlingException(message, e); - } - finally { - AckUtils.autoAck(ackCallback); - } - } - - private void requeueOrNack(Message message, AcknowledgmentCallback ackCallback, MessagingException e) { - if (!ackCallback.isAcknowledged() && shouldRequeue(e)) { - AckUtils.requeue(ackCallback); - } - else { - AckUtils.autoNack(ackCallback); - if (e.getFailedMessage().equals(message)) { - throw e; - } - throw new MessageHandlingException(message, e); - } - } - - protected boolean shouldRequeue(Exception e) { - boolean requeue = false; - Throwable t = e.getCause(); - while (t != null && !requeue) { - requeue = t instanceof RequeueCurrentMessageException; - t = t.getCause(); - } - return requeue; - } - - @Override - public boolean open(RetryContext context, - RetryCallback callback) { - if (DefaultPollableMessageSource.this.recoveryCallback != null) { - attributesHolder.set(context); - } - return true; - } - - @Override - public void close(RetryContext context, - RetryCallback callback, Throwable throwable) { - attributesHolder.remove(); - } - - @Override - public void onError(RetryContext context, - RetryCallback callback, Throwable throwable) { - // Empty - } - - /** - * Receives Message from the source and converts its payload to a provided type. Can - * return null - * @param type type reference - * @return received message - */ - private Message receive(ParameterizedTypeReference type) { - Message message = this.source.receive(); - if (message != null && type != null && this.messageConverter != null) { - Object payload; - if (FunctionTypeUtils.isTypeCollection(type.getType()) || FunctionTypeUtils.isTypeMap(type.getType())) { - payload = this.messageConverter.fromMessage(message, FunctionTypeUtils.getRawType(type.getType()), type.getType()); - } - else { - payload = this.messageConverter.fromMessage(message, (Class) type.getType()); - } - if (payload == null) { - throw new MessageConversionException(message, "No converter could convert Message"); - } - message = MessageBuilder.withPayload(payload).copyHeaders(message.getHeaders()).build(); - } - return message; - } - - private void doHandleMessage(MessageHandler handler, Message message) { - try { - handler.handleMessage(message); - } - catch (Throwable t) { // NOSONAR - throw new MessageHandlingException(message, t); - } - } - - /** - * If there's a retry template, it will set the attributes holder via the listener. If - * there's no retry template, but there's an error channel, we create a new attributes - * holder here. If an attributes holder exists (by either method), we set the - * attributes for use by the {@link ErrorMessageStrategy}. - * @param message the Spring Messaging message to use. - */ - private void setAttributesIfNecessary(Message message) { - boolean needHolder = this.errorChannel != null && this.retryTemplate == null; - boolean needAttributes = needHolder || this.retryTemplate != null; - if (needHolder) { - attributesHolder.set(ErrorMessageUtils.getAttributeAccessor(null, null)); - } - if (needAttributes) { - AttributeAccessor attributes = attributesHolder.get(); - if (attributes != null) { - attributes.setAttribute(ErrorMessageUtils.INPUT_MESSAGE_CONTEXT_KEY, - message); - if (this.attributesProvider != null) { - this.attributesProvider.accept(attributes, message); - } - } - } - } - - private void handle(Message message, MessageHandler handler) { - setAttributesIfNecessary(message); - doHandleMessage(handler, message); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DirectHandler.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DirectHandler.java deleted file mode 100644 index ef8b4c536..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/DirectHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; - -/** - * @author Marius Bogoevici - */ -public class DirectHandler implements MessageHandler { - - private final MessageChannel outputChannel; - - public DirectHandler(MessageChannel outputChannel) { - this.outputChannel = outputChannel; - } - - @Override - public void handleMessage(Message message) throws MessagingException { - this.outputChannel.send(message); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/EmbeddedHeaderUtils.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/EmbeddedHeaderUtils.java deleted file mode 100644 index 480f620c2..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/EmbeddedHeaderUtils.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2014-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.cloud.stream.binder; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.integration.support.json.Jackson2JsonObjectMapper; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.util.ObjectUtils; - -/** - * Encodes requested headers into payload with format - * {@code 0xff, n(1), [ [lenHdr(1), hdr, lenValue(4), value] ... ]}. The 0xff indicates - * this new format; n is number of headers (max 255); for each header, the name length (1 - * byte) is followed by the name, followed by the value length (int) followed by the value - * (json). - *

- * Previously, there was no leading 0xff; the value length was 1 byte and only String - * header values were supported (no JSON conversion). - * - * @author Eric Bottard - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @author Marius Bogoevici - * @since 1.2 - */ -public abstract class EmbeddedHeaderUtils { - - private static final Jackson2JsonObjectMapper objectMapper = new Jackson2JsonObjectMapper(); - - public static String decodeExceptionMessage(Message requestMessage) { - return "Could not convert message: " + requestMessage; - } - - /** - * Return a new message where some of the original headers of {@code original} have - * been embedded into the new message payload. - * @param original original message - * @param headers headers to embedd - * @return a new message - */ - public static byte[] embedHeaders(MessageValues original, String... headers) { - try { - byte[][] headerValues = new byte[headers.length][]; - int n = 0; - int headerCount = 0; - int headersLength = 0; - for (String header : headers) { - Object value = original.get(header); - if (value != null) { - String json = objectMapper.toJson(value); - headerValues[n] = json.getBytes(StandardCharsets.UTF_8); - headerCount++; - headersLength += header.length() + headerValues[n++].length; - } - else { - headerValues[n++] = null; - } - } - // 0xff, n(1), [ [lenHdr(1), hdr, lenValue(4), value] ... ] - byte[] newPayload = new byte[((byte[]) original.getPayload()).length - + headersLength + headerCount * 5 + 2]; - ByteBuffer byteBuffer = ByteBuffer.wrap(newPayload); - byteBuffer.put((byte) 0xff); // signal new format - byteBuffer.put((byte) headerCount); - for (int i = 0; i < headers.length; i++) { - if (headerValues[i] != null) { - byteBuffer.put((byte) headers[i].length()); - byteBuffer.put(headers[i].getBytes(StandardCharsets.UTF_8)); - byteBuffer.putInt(headerValues[i].length); - byteBuffer.put(headerValues[i]); - } - } - - byteBuffer.put((byte[]) original.getPayload()); - return byteBuffer.array(); - } - catch (Exception e) { - throw new IllegalStateException(e); - } - - } - - /** - * Return a message where headers, that were originally embedded into the payload, - * have been promoted back to actual headers. The new payload is now the original - * payload. - * @param message the message to extract headers - * @param copyRequestHeaders boolean value to specify if the request headers should be - * copied - * @return wrapped message values - * @throws Exception when extraction failed - */ - public static MessageValues extractHeaders(Message message, - boolean copyRequestHeaders) throws Exception { - return extractHeaders(message.getPayload(), copyRequestHeaders, - message.getHeaders()); - } - - private static MessageValues extractHeaders(byte[] payload, - boolean copyRequestHeaders, MessageHeaders requestHeaders) throws Exception { - ByteBuffer byteBuffer = ByteBuffer.wrap(payload); - int headerCount = byteBuffer.get() & 0xff; - if (headerCount == 0xff) { - headerCount = byteBuffer.get() & 0xff; - Map headers = new HashMap(); - for (int i = 0; i < headerCount; i++) { - int len = byteBuffer.get() & 0xff; - String headerName = new String(payload, byteBuffer.position(), len, - StandardCharsets.UTF_8); - byteBuffer.position(byteBuffer.position() + len); - len = byteBuffer.getInt(); - String headerValue = new String(payload, byteBuffer.position(), len, - StandardCharsets.UTF_8); - Object headerContent = objectMapper.fromJson(headerValue, Object.class); - headers.put(headerName, headerContent); - byteBuffer.position(byteBuffer.position() + len); - } - byte[] newPayload = new byte[byteBuffer.remaining()]; - byteBuffer.get(newPayload); - return buildMessageValues(newPayload, headers, copyRequestHeaders, - requestHeaders); - } - else { - return buildMessageValues(payload, new HashMap<>(), copyRequestHeaders, - requestHeaders); - } - } - - /** - * Return a message where headers, that were originally embedded into the payload, - * have been promoted back to actual headers. The new payload is now the original - * payload. - * @param payload the message payload - * @return the message with extracted headers - * @throws Exception when extraction failed - */ - public static MessageValues extractHeaders(byte[] payload) throws Exception { - return extractHeaders(payload, false, null); - } - - private static MessageValues buildMessageValues(byte[] payload, - Map headers, boolean copyRequestHeaders, - MessageHeaders requestHeaders) { - MessageValues messageValues = new MessageValues(payload, headers); - if (copyRequestHeaders && requestHeaders != null) { - messageValues.copyHeadersIfAbsent(requestHeaders); - } - return messageValues; - } - - public static String[] headersToEmbed(String[] configuredHeaders) { - String[] headersToMap; - if (ObjectUtils.isEmpty(configuredHeaders)) { - headersToMap = BinderHeaders.STANDARD_HEADERS; - } - else { - String[] combinedHeadersToMap = Arrays.copyOfRange( - BinderHeaders.STANDARD_HEADERS, 0, - BinderHeaders.STANDARD_HEADERS.length + configuredHeaders.length); - System.arraycopy(configuredHeaders, 0, combinedHeadersToMap, - BinderHeaders.STANDARD_HEADERS.length, configuredHeaders.length); - headersToMap = combinedHeadersToMap; - } - return headersToMap; - } - - /** - * Return true if the bytes might have embedded headers. (First byte is 0xff and long - * enough for at least one header). - * @param bytes the array. - * @return true if it may have embedded headers. - */ - public static boolean mayHaveEmbeddedHeaders(byte[] bytes) { - return bytes.length > 8 && (bytes[0] & 0xff) == 0xff; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedBindingProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedBindingProperties.java deleted file mode 100644 index 23dc5b4b8..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedBindingProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.Collections; -import java.util.Map; - -/** - * Properties that extend the common binding properties for a particular binder - * implementation. - * - * @param consumer properties type - * @param

producer properties type - * @author Marius Bogoevici - * @author Mark Fisher - * @author Soby Chacko - */ -public interface ExtendedBindingProperties { - - C getExtendedConsumerProperties(String channelName); - - P getExtendedProducerProperties(String channelName); - - default Map getBindings() { - return Collections.emptyMap(); - } - - /** - * Extended binding properties can define a default prefix to place all the extended - * common producer and consumer properties. For example, if the binder type is foo it - * is convenient to specify common extended properties for the producer or consumer - * across multiple bindings in the form of - * `spring.cloud.stream.foo.default.producer.x=y` or - * `spring.cloud.stream.foo.default.consumer.x=y`. - * - * The binding process will use this defaults prefix to resolve any common extended - * producer and consumer properties. - * @return default prefix for extended properties - * @since 2.1.0 - */ - String getDefaultsPrefix(); - - /** - * - * Extended properties class which should be a subclass of - * {@link BinderSpecificPropertiesProvider} against which default extended producer - * and consumer properties are resolved. - * @return extended properties class that contains extended producer/consumer - * properties - * @since 2.1.0 - */ - Class getExtendedPropertiesEntryClass(); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedConsumerProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedConsumerProperties.java deleted file mode 100644 index 03c491c07..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedConsumerProperties.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -/** - * Extension of {@link ConsumerProperties} to be used with an - * {@link ExtendedPropertiesBinder}. - * - * @param extension type - * @author Marius Bogoevici - */ -public class ExtendedConsumerProperties extends ConsumerProperties { - - private T extension; - - public ExtendedConsumerProperties(T extension) { - this.extension = extension; - } - - public T getExtension() { - return this.extension; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedProducerProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedProducerProperties.java deleted file mode 100644 index c01b67193..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedProducerProperties.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -/** - * @param extension type - * @author Marius Bogoevici - */ -public class ExtendedProducerProperties extends ProducerProperties { - - private T extension; - - public ExtendedProducerProperties(T extension) { - this.extension = extension; - } - - public T getExtension() { - return this.extension; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedPropertiesBinder.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedPropertiesBinder.java deleted file mode 100644 index f440c143d..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ExtendedPropertiesBinder.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -/** - * Extension of {@link Binder} that takes {@link ExtendedConsumerProperties} and - * {@link ExtendedProducerProperties} as arguments. In addition to supporting binding - * operations, it allows the binder to provide values for the additional properties it - * expects on the bindings. - * - * @param binder type - * @param consumer type - * @param

producer type - * @author Marius Bogoevici - */ -public interface ExtendedPropertiesBinder - extends Binder, ExtendedProducerProperties

>, - ExtendedBindingProperties { - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/FinalRethrowingErrorMessageHandler.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/FinalRethrowingErrorMessageHandler.java deleted file mode 100644 index 7d2531c22..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/FinalRethrowingErrorMessageHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; - -/** - * A MessageHandler that always is the last subscriber (on a {@link BinderErrorChannel}) - * that throws an exception if it the only subscriber (aside from the bridge to the global - * error channel). It is typically only used if a binder implementation does not return a - * handled from {@code getErrorMessageHandler()}. - * - * @author Gary Russell - * @author Byungjun You - * @since 1.3 - * - */ -class FinalRethrowingErrorMessageHandler - implements MessageHandler, LastSubscriberMessageHandler { - - @Override - public void handleMessage(Message message) throws MessagingException { - if (message.getPayload() instanceof MessagingException messagingException) { - throw messagingException; - } - else { - throw new MessagingException((Message) null, - (Throwable) message.getPayload()); - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/HeaderMode.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/HeaderMode.java deleted file mode 100644 index 41a729270..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/HeaderMode.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -/** - * @author Marius Bogoevici - * @author Gary Russell - */ -public enum HeaderMode { - - /** - * No headers. - */ - none, - - /** - * Native headers. - */ - headers, - - /** - * Headers embedded in payload - e.g. kafka < 0.11 - */ - embeddedHeaders - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/JavaClassMimeTypeUtils.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/JavaClassMimeTypeUtils.java deleted file mode 100644 index eced820c0..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/JavaClassMimeTypeUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.springframework.util.Assert; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; - -/** - * Handles representing any java class as a {@link MimeType}. - * - * @author David Turanski - * @author Ilayaperumal Gopinathan - * @author Soby Chacko - */ -public abstract class JavaClassMimeTypeUtils { - - private static ConcurrentMap mimeTypesCache = new ConcurrentHashMap<>(); - - /** - * Convert payload to {@link MimeType} based on the content type on the message. - * @param payload the payload to convert - * @param originalContentType content type on the message - * @return converted {@link MimeType} - */ - public static MimeType mimeTypeFromObject(Object payload, - String originalContentType) { - Assert.notNull(payload, "payload object cannot be null."); - if (payload instanceof byte[]) { - return MimeTypeUtils.APPLICATION_OCTET_STREAM; - } - if (payload instanceof String) { - return MimeTypeUtils.APPLICATION_JSON_VALUE.equals(originalContentType) - ? MimeTypeUtils.APPLICATION_JSON : MimeTypeUtils.TEXT_PLAIN; - } - String className = payload.getClass().getName(); - MimeType mimeType = mimeTypesCache.get(className); - if (mimeType == null) { - String modifiedClassName = className; - if (payload.getClass().isArray()) { - // Need to remove trailing ';' for an object array, e.g. - // "[Ljava.lang.String;" or multi-dimensional - // "[[[Ljava.lang.String;" - if (modifiedClassName.endsWith(";")) { - modifiedClassName = modifiedClassName.substring(0, - modifiedClassName.length() - 1); - } - // Wrap in quotes to handle the illegal '[' character - modifiedClassName = "\"" + modifiedClassName + "\""; - } - mimeType = MimeType - .valueOf("application/x-java-object;type=" + modifiedClassName); - mimeTypesCache.put(className, mimeType); - } - return mimeType; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/LastSubscriberAwareChannel.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/LastSubscriberAwareChannel.java deleted file mode 100644 index 4d4609dfb..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/LastSubscriberAwareChannel.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder; - -import org.springframework.messaging.SubscribableChannel; - -/** - * A channel that can ensure a {@code LastSubscriberMessageHandler} is always the last - * subscriber. - * - * @author Gary Russell - * @since 1.3 - * - */ -interface LastSubscriberAwareChannel extends SubscribableChannel { - - /** - * Return the current subscribers. - * @return the subscribers. - */ - int subscribers(); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/LastSubscriberMessageHandler.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/LastSubscriberMessageHandler.java deleted file mode 100644 index e1fa2fd44..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/LastSubscriberMessageHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binder; - -import org.springframework.messaging.MessageHandler; - -/** - * A marker interface designating a subscriber that must be the last. - * - * @author Gary Russell - * @since 1.3 - * - */ -public interface LastSubscriberMessageHandler extends MessageHandler { - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageValues.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageValues.java deleted file mode 100644 index 772136fb8..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageValues.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2015-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.springframework.integration.support.MessageBuilder; -import org.springframework.integration.support.MessageBuilderFactory; -import org.springframework.messaging.Message; -import org.springframework.util.Assert; - -/** - * A mutable type for allowing {@link Binder} implementations to transform and enrich - * message content more efficiently. - * - * @author David Turanski - * @author Marius Bogoevici - */ -public class MessageValues implements Map { - - private Map headers = new HashMap<>(); - - private Object payload; - - /** - * Create an instance from a {@link Message}. - * @param message the message - */ - public MessageValues(Message message) { - this.payload = message.getPayload(); - for (Map.Entry header : message.getHeaders().entrySet()) { - this.headers.put(header.getKey(), header.getValue()); - } - } - - public MessageValues(Object payload, Map headers) { - this.payload = payload; - this.headers.putAll(headers); - } - - /** - * @return the payload - */ - public Object getPayload() { - return this.payload; - } - - /** - * Set the payload. - * @param payload any non null object. - */ - public void setPayload(Object payload) { - Assert.notNull(payload, "'payload' cannot be null"); - this.payload = payload; - } - - public Map getHeaders() { - return this.headers; - } - - /** - * Convert to a {@link Message} using a - * {@link org.springframework.integration.support.MessageBuilderFactory}. - * @param messageBuilderFactory the MessageBuilderFactory - * @return the Message - */ - public Message toMessage(MessageBuilderFactory messageBuilderFactory) { - return messageBuilderFactory.withPayload(this.payload).copyHeaders(this.headers) - .build(); - } - - /** - * Convert to a {@link Message} using a the default - * {@link org.springframework.integration.support.MessageBuilder}. - * @return the Message - */ - public Message toMessage() { - return MessageBuilder.withPayload(this.payload).copyHeaders(this.headers).build(); - } - - @Override - public int size() { - return this.headers.size(); - } - - @Override - public boolean isEmpty() { - return this.headers.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.headers.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.headers.containsValue(value); - } - - @Override - public Object get(Object key) { - return this.headers.get(key); - } - - @Override - public Object put(String key, Object value) { - return this.headers.put(key, value); - } - - @Override - public Object remove(Object key) { - return this.headers.remove(key); - } - - @Override - public void putAll(Map m) { - this.headers.putAll(m); - } - - @Override - public void clear() { - this.headers.clear(); - } - - @Override - public Set keySet() { - return this.headers.keySet(); - } - - @Override - public Collection values() { - return this.headers.values(); - } - - @Override - public Set> entrySet() { - return this.headers.entrySet(); - } - - public void copyHeadersIfAbsent(Map headersToCopy) { - for (Entry headersToCopyEntry : headersToCopy.entrySet()) { - if (!containsKey(headersToCopyEntry.getKey())) { - put(headersToCopyEntry.getKey(), headersToCopyEntry.getValue()); - } - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionHandler.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionHandler.java deleted file mode 100644 index a369fcef6..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionHandler.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -import java.lang.reflect.Field; -import java.util.Map; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.context.expression.BeanFactoryResolver; -import org.springframework.expression.EvaluationContext; -import org.springframework.messaging.Message; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * Utility class to determine if a binding is configured for partitioning (based on the - * binder properties provided in the constructor) and what partition a message should be - * delivered to. - * - * @author Patrick Peralta - * @author David Turanski - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @author Mark Fisher - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public class PartitionHandler { - - private final EvaluationContext evaluationContext; - - private final ProducerProperties producerProperties; - - private final PartitionKeyExtractorStrategy partitionKeyExtractorStrategy; - - private final PartitionSelectorStrategy partitionSelectorStrategy; - - private final ConfigurableListableBeanFactory beanFactory; - - private volatile int partitionCount; - - /** - * Construct a {@code PartitionHandler}. - * @param evaluationContext evaluation context for binder - * @param properties binder properties - * @param partitionKeyExtractorStrategy PartitionKeyExtractor strategy - * @param partitionSelectorStrategy PartitionSelector strategy - * - * @deprecated since 3.0.2. Please use another constructor which allows you to pass an instance of beanFactory - */ - @Deprecated - public PartitionHandler(EvaluationContext evaluationContext, - ProducerProperties properties, - PartitionKeyExtractorStrategy partitionKeyExtractorStrategy, - PartitionSelectorStrategy partitionSelectorStrategy) { - - this(evaluationContext, properties, (ConfigurableListableBeanFactory) extractBeanFactoryFromEvaluationContext(evaluationContext)); - } - - /** - * Construct a {@code PartitionHandler}. - * @param evaluationContext evaluation context for binder - * @param properties binder properties - * @param beanFactory instance of ConfigurableListableBeanFactory - * - * @since 3.0.2 - */ - public PartitionHandler(EvaluationContext evaluationContext, - ProducerProperties properties, ConfigurableListableBeanFactory beanFactory) { - this.beanFactory = beanFactory; - this.evaluationContext = evaluationContext; - this.producerProperties = properties; - this.partitionKeyExtractorStrategy = this.getPartitionKeyExtractorStrategy(properties); - this.partitionSelectorStrategy = this.getPartitionSelectorStrategy(properties); - this.partitionCount = this.producerProperties.getPartitionCount(); - - } - - /** - * Set the actual partition count (if different to the configured count). - * @param partitionCount the count. - */ - public void setPartitionCount(int partitionCount) { - this.partitionCount = partitionCount; - } - - /** - * Determine the partition to which to send this message. - *

- * If a partition key extractor class is provided, it is invoked to determine the key. - * Otherwise, the partition key expression is evaluated to obtain the key value. - *

- * If a partition selector class is provided, it will be invoked to determine the - * partition. Otherwise, if the partition expression is not null, it is evaluated - * against the key and is expected to return an integer to which the modulo function - * will be applied, using the {@code partitionCount} as the divisor. If no partition - * expression is provided, the key will be passed to the binder partition strategy - * along with the {@code partitionCount}. The default partition strategy uses - * {@code key.hashCode()}, and the result will be the mod of that value. - * @param message the message. - * @return the partition - */ - public int determinePartition(Message message) { - Object key = extractKey(message); - - int partition; - if (this.producerProperties.getPartitionSelectorExpression() != null) { - partition = this.producerProperties.getPartitionSelectorExpression() - .getValue(this.evaluationContext, key, Integer.class); - } - else { - partition = this.partitionSelectorStrategy.selectPartition(key, - this.partitionCount); - } - // protection in case a user selector returns a negative. - return Math.abs(partition % this.partitionCount); - } - - private Object extractKey(Message message) { - Object key = invokeKeyExtractor(message); - if (key == null && this.producerProperties.getPartitionKeyExpression() != null) { - key = this.producerProperties.getPartitionKeyExpression() - .getValue(this.evaluationContext, message); - } - Assert.notNull(key, "Partition key cannot be null"); - - return key; - } - - private Object invokeKeyExtractor(Message message) { - if (this.partitionKeyExtractorStrategy != null) { - return this.partitionKeyExtractorStrategy.extractKey(message); - } - return null; - } - - private PartitionKeyExtractorStrategy getPartitionKeyExtractorStrategy( - ProducerProperties producerProperties) { - PartitionKeyExtractorStrategy partitionKeyExtractor; - if (StringUtils.hasText(producerProperties.getPartitionKeyExtractorName())) { - partitionKeyExtractor = (PartitionKeyExtractorStrategy) this.beanFactory.getBean( - producerProperties.getPartitionKeyExtractorName(), - PartitionKeyExtractorStrategy.class); - Assert.notNull(partitionKeyExtractor, - "PartitionKeyExtractorStrategy bean with the name '" - + producerProperties.getPartitionKeyExtractorName() - + "' can not be found. Has it been configured (e.g., @Bean)?"); - } - else { - Map extractors = this.beanFactory - .getBeansOfType(PartitionKeyExtractorStrategy.class); - Assert.isTrue(extractors.size() <= 1, - "Multiple beans of type 'PartitionKeyExtractorStrategy' found. " - + extractors + ". Please " - + "use 'spring.cloud.stream.bindings.output.producer.partitionKeyExtractorName' property to specify " - + "the name of the bean to be used."); - partitionKeyExtractor = CollectionUtils.isEmpty(extractors) ? null - : extractors.values().iterator().next(); - } - return partitionKeyExtractor; - } - - private PartitionSelectorStrategy getPartitionSelectorStrategy( - ProducerProperties producerProperties) { - PartitionSelectorStrategy partitionSelector; - if (StringUtils.hasText(producerProperties.getPartitionSelectorName())) { - partitionSelector = this.beanFactory.getBean( - producerProperties.getPartitionSelectorName(), - PartitionSelectorStrategy.class); - Assert.notNull(partitionSelector, - "PartitionSelectorStrategy bean with the name '" - + producerProperties.getPartitionSelectorName() - + "' can not be found. Has it been configured (e.g., @Bean)?"); - } - else { - Map selectors = this.beanFactory - .getBeansOfType(PartitionSelectorStrategy.class); - Assert.isTrue(selectors.size() <= 1, - "Multiple beans of type 'PartitionSelectorStrategy' found. " - + selectors + ". Please " - + "use 'spring.cloud.stream.bindings.output.producer.partitionSelectorName' property to specify " - + "the name of the bean to be used."); - partitionSelector = CollectionUtils.isEmpty(selectors) - ? new DefaultPartitionSelector() - : selectors.values().iterator().next(); - } - return partitionSelector; - } - - private static BeanFactory extractBeanFactoryFromEvaluationContext(EvaluationContext evaluationContext) { - try { - Field field = ReflectionUtils.findField(BeanFactoryResolver.class, "beanFactory"); - field.setAccessible(true); - return (BeanFactory) field.get(evaluationContext); - } - catch (Exception e) { - throw new RuntimeException("Failed to extract beanFactory from EvaluationContext. Please use different constructor" - + " which allows you to pass the instance of the beanFactory."); - } - } - - /** - * Default partition strategy; only works on keys with "real" hash codes, such as - * String. Caller now always applies modulo so no need to do so here. - */ - private static class DefaultPartitionSelector implements PartitionSelectorStrategy { - - @Override - public int selectPartition(Object key, int partitionCount) { - int hashCode = key.hashCode(); - if (hashCode == Integer.MIN_VALUE) { - hashCode = 0; - } - return Math.abs(hashCode); - } - } - - - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionKeyExtractorStrategy.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionKeyExtractorStrategy.java deleted file mode 100644 index e842bbd0c..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionKeyExtractorStrategy.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2014-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.cloud.stream.binder; - -import org.springframework.messaging.Message; - -/** - * Strategy for extracting a partition key from a Message. - * - * @author Gary Russell - */ -public interface PartitionKeyExtractorStrategy { - - Object extractKey(Message message); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionSelectorStrategy.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionSelectorStrategy.java deleted file mode 100644 index c63b3fb79..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PartitionSelectorStrategy.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2014-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.cloud.stream.binder; - -/** - * Strategy for determining the partition to which a message should be sent. - * - * @author Gary Russell - */ -public interface PartitionSelectorStrategy { - - /** - * Determine the partition based on a key. The partitionCount is 1 greater than the - * maximum value of a valid partition. Typical implementations will return - * {@code someValue % partitionCount}. The caller will apply that same modulo - * operation (as well as enforcing absolute value) if the value exceeds partitionCount - * - 1. - * @param key the key - * @param partitionCount the number of partitions - * @return the partition - */ - int selectPartition(Object key, int partitionCount); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableConsumerBinder.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableConsumerBinder.java deleted file mode 100644 index 94ed3ef54..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableConsumerBinder.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -/** - * A binder that supports pollable message sources. - * - * @param the polled consumer handler type. - * @param the consumer properties type. - * @author Gary Russell - * @since 2.0 - * - */ -public interface PollableConsumerBinder { - - /** - * Configure a binding for a pollable message source. - * @param name the binding name. - * @param group the consumer group. - * @param inboundBindTarget the binding target. - * @param consumerProperties the consumer properties. - * @return the binding. - */ - default Binding> bindPollableConsumer(String name, String group, - PollableSource inboundBindTarget, C consumerProperties) { - throw new UnsupportedOperationException( - "This binder does not support pollable consumers"); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableMessageSource.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableMessageSource.java deleted file mode 100644 index 17ee952cb..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableMessageSource.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import org.springframework.messaging.MessageHandler; - -/** - * A {@link PollableSource} that calls a {@link MessageHandler} with a - * {@link org.springframework.messaging.Message}. - * - * @author Gary Russell - * @since 2.0 - * - */ -public interface PollableMessageSource extends PollableSource { - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableSource.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableSource.java deleted file mode 100644 index d2772d413..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/PollableSource.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import org.springframework.core.ParameterizedTypeReference; - -/** - * A mechanism to poll a consumer. - * - * @param the handler type to process the result of the poll. - * @author Gary Russell - * @since 2.0 - * - */ -@FunctionalInterface -public interface PollableSource { - - /** - * Poll the consumer. - * @param handler the handler. - * @return true if a message was handled. - */ - boolean poll(H handler); - - /** - * Poll the consumer and convert the payload to the type. Throw a - * {@code RequeueCurrentMessageException} to force the current message to be requeued - * in the broker (after retries are exhausted, if configured). - * @param handler the handler. - * @param type the type. - * @return true if a message was handled. - */ - default boolean poll(H handler, ParameterizedTypeReference type) { - return poll(handler); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ProducerProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ProducerProperties.java deleted file mode 100644 index 9713d1828..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ProducerProperties.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binder; - -import java.io.IOException; -import java.time.Duration; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import org.springframework.expression.Expression; - -/** - * Common producer properties. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Oleg Zhurakousky - */ -@JsonInclude(Include.NON_DEFAULT) -public class ProducerProperties { - - /** - * Binding name for this producer binding. - */ - private String bindingName; - - /** - * Signals if this producer needs to be started automatically. Default: true - */ - private boolean autoStartup = true; - - @JsonSerialize(using = ExpressionSerializer.class) - private Expression partitionKeyExpression; - - /** - * The name of the bean that implements {@link PartitionKeyExtractorStrategy}\. Used - * to extract a key used to compute the partition id (see 'partitionSelector*')
- * Mutually exclusive with 'partitionKeyExpression'. - */ - private String partitionKeyExtractorName; - - /** - * The name of the bean that implements {@link PartitionSelectorStrategy}\. Used to - * determine partition id based on partition key (see 'partitionKeyExtractor*').
- * Mutually exclusive with 'partitionSelectorExpression'. - */ - private String partitionSelectorName; - - @JsonSerialize(using = ExpressionSerializer.class) - private Expression partitionSelectorExpression; - - private int partitionCount = 1; - - private String[] requiredGroups = new String[] {}; - - private HeaderMode headerMode; - - private boolean useNativeEncoding = false; - - private boolean errorChannelEnabled = false; - - private PollerProperties poller; - - private boolean dynamicPartitionUpdatesEnabled = false; - - public String getBindingName() { - return bindingName; - } - - /** - * This method is not intended as a configuration property to set by the applications. - * Therefore, we are not providing a proper setter method for this. - * @param bindingName binding name populated by the framework. - */ - public void populateBindingName(String bindingName) { - this.bindingName = bindingName; - } - - public Expression getPartitionKeyExpression() { - return this.partitionKeyExpression; - } - - public void setPartitionKeyExpression(Expression partitionKeyExpression) { - this.partitionKeyExpression = partitionKeyExpression; - } - - public boolean isPartitioned() { - return this.partitionKeyExpression != null - || this.partitionKeyExtractorName != null; - } - - public Expression getPartitionSelectorExpression() { - return this.partitionSelectorExpression; - } - - public void setPartitionSelectorExpression(Expression partitionSelectorExpression) { - this.partitionSelectorExpression = partitionSelectorExpression; - } - - public int getPartitionCount() { - return this.partitionCount; - } - - public void setPartitionCount(int partitionCount) { - this.partitionCount = partitionCount; - } - - public String[] getRequiredGroups() { - return this.requiredGroups; - } - - public void setRequiredGroups(String... requiredGroups) { - this.requiredGroups = requiredGroups; - } - - public boolean isValidPartitionKeyProperty() { - return this.partitionKeyExpression == null; - } - - public boolean isValidPartitionSelectorProperty() { - return this.partitionSelectorExpression == null; - } - - public HeaderMode getHeaderMode() { - return this.headerMode; - } - - public void setHeaderMode(HeaderMode headerMode) { - this.headerMode = headerMode; - } - - public boolean isUseNativeEncoding() { - return this.useNativeEncoding; - } - - public void setUseNativeEncoding(boolean useNativeEncoding) { - this.useNativeEncoding = useNativeEncoding; - } - - public boolean isErrorChannelEnabled() { - return this.errorChannelEnabled; - } - - public void setErrorChannelEnabled(boolean errorChannelEnabled) { - this.errorChannelEnabled = errorChannelEnabled; - } - - public String getPartitionKeyExtractorName() { - return this.partitionKeyExtractorName; - } - - public void setPartitionKeyExtractorName(String partitionKeyExtractorName) { - this.partitionKeyExtractorName = partitionKeyExtractorName; - } - - public String getPartitionSelectorName() { - return this.partitionSelectorName; - } - - public void setPartitionSelectorName(String partitionSelectorName) { - this.partitionSelectorName = partitionSelectorName; - } - - public boolean isAutoStartup() { - return this.autoStartup; - } - - public void setAutoStartup(boolean autoStartup) { - this.autoStartup = autoStartup; - } - - public PollerProperties getPoller() { - return poller; - } - - public void setPoller(PollerProperties poller) { - this.poller = poller; - } - - /** - * Returns status of property dynamicPartitionUpdatesEnabled. - * @return true if dynamic updates should are enabled otherwise false - */ - public boolean isDynamicPartitionUpdatesEnabled() { - return dynamicPartitionUpdatesEnabled; - } - - /** - * A flag which enables/disables partition count updates during runtime. Disabled by default. - * Depends on binder if supported or not. - * Currently only supported by kafka binder (see 'Partitioning with the Kafka Binder' documentation for details) - * @param enabled true if dynamic updates should be enabled otherwise false - */ - public void setDynamicPartitionUpdatesEnabled(boolean enabled) { - this.dynamicPartitionUpdatesEnabled = enabled; - } - - static class ExpressionSerializer extends JsonSerializer { - - @Override - public void serialize(Expression expression, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - if (expression != null) { - jsonGenerator.writeString(expression.getExpressionString()); - } - } - - } - - public static class PollerProperties { - - private Duration fixedDelay = Duration.ofMillis(1000); - - private long maxMessagesPerPoll = 1L; - - private String cron; - - private Duration initialDelay = Duration.ofMillis(0); - - public long getMaxMessagesPerPoll() { - return maxMessagesPerPoll; - } - - public void setMaxMessagesPerPoll(long maxMessagesPerPoll) { - this.maxMessagesPerPoll = maxMessagesPerPoll; - } - - public String getCron() { - return cron; - } - - public void setCron(String cron) { - this.cron = cron; - } - - public Duration getInitialDelay() { - return initialDelay; - } - - public void setInitialDelay(Duration initialDelay) { - this.initialDelay = initialDelay; - } - - public Duration getFixedDelay() { - return fixedDelay; - } - - public void setFixedDelay(Duration fixedDelay) { - this.fixedDelay = fixedDelay; - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/RequeueCurrentMessageException.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/RequeueCurrentMessageException.java deleted file mode 100644 index eb9f162eb..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/RequeueCurrentMessageException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -/** - * When using a {@code PollableMessageSource} throw this exception to cause the current - * message to be requeued in the broker so that it will be redelivered on the next poll. - * - * @author Gary Russell - * @since 2.1 - * - */ -public class RequeueCurrentMessageException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public RequeueCurrentMessageException() { - super(); - } - - public RequeueCurrentMessageException(String message, Throwable cause) { - super(message, cause); - } - - public RequeueCurrentMessageException(String message) { - super(message); - } - - public RequeueCurrentMessageException(Throwable cause) { - super(cause); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindableProxyFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindableProxyFactory.java deleted file mode 100644 index a64ab7a20..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindableProxyFactory.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.binding; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.util.StringUtils; - -/** - * Base class for bindable proxy factories. This class is mainly refactored from the - * {@link BindableProxyFactory} so that other downstream binders who want to bind their own - * targets can make use of it. - * - * Original authors in {@link BindableProxyFactory} - * @author Soby Chacko - * @since 3.0.0 - */ -public class AbstractBindableProxyFactory implements Bindable { - - protected Map bindingTargetFactories; - - protected Map inputHolders = new LinkedHashMap<>(); - - protected Map outputHolders = new LinkedHashMap<>(); - - protected Class type; - - public AbstractBindableProxyFactory(Class type) { - this.type = type; - } - - protected void populateBindingTargetFactories(BeanFactory beanFactory) { - this.bindingTargetFactories = ((ListableBeanFactory) beanFactory).getBeansOfType(BindingTargetFactory.class); - } - - protected BindingTargetFactory getBindingTargetFactory(Class bindingTargetType) { - List candidateBindingTargetFactories = new ArrayList<>(); - for (Map.Entry bindingTargetFactoryEntry : this.bindingTargetFactories - .entrySet()) { - if (bindingTargetFactoryEntry.getValue().canCreate(bindingTargetType)) { - candidateBindingTargetFactories.add(bindingTargetFactoryEntry.getKey()); - } - } - if (candidateBindingTargetFactories.size() == 1) { - return this.bindingTargetFactories - .get(candidateBindingTargetFactories.get(0)); - } - else { - if (candidateBindingTargetFactories.size() == 0) { - throw new IllegalStateException( - "No factory found for binding target type: " - + bindingTargetType.getName() - + " among registered factories: " - + StringUtils.collectionToCommaDelimitedString( - this.bindingTargetFactories.keySet())); - } - else { - throw new IllegalStateException( - "Multiple factories found for binding target type: " - + bindingTargetType.getName() + ": " - + StringUtils.collectionToCommaDelimitedString( - candidateBindingTargetFactories)); - } - } - } - - @Override - public Collection> createAndBindInputs( - BindingService bindingService) { - List> bindings = new ArrayList<>(); - for (Map.Entry boundTargetHolderEntry : this.inputHolders - .entrySet()) { - String inputTargetName = boundTargetHolderEntry.getKey(); - BoundTargetHolder boundTargetHolder = boundTargetHolderEntry.getValue(); - if (boundTargetHolder.isBindable()) { - bindings.addAll(bindingService.bindConsumer( - boundTargetHolder.getBoundTarget(), inputTargetName)); - } - } - return bindings; - } - - @Override - public Collection> createAndBindOutputs( - BindingService bindingService) { - List> bindings = new ArrayList<>(); - - for (Map.Entry boundTargetHolderEntry : this.outputHolders - .entrySet()) { - BoundTargetHolder boundTargetHolder = boundTargetHolderEntry.getValue(); - String outputTargetName = boundTargetHolderEntry.getKey(); - if (boundTargetHolderEntry.getValue().isBindable()) { - bindings.add(bindingService.bindProducer( - boundTargetHolder.getBoundTarget(), outputTargetName)); - } - } - return bindings; - } - - @Override - public void unbindInputs(BindingService bindingService) { - for (Map.Entry boundTargetHolderEntry : this.inputHolders - .entrySet()) { - if (boundTargetHolderEntry.getValue().isBindable()) { - bindingService.unbindConsumers(boundTargetHolderEntry.getKey()); - } - } - } - - @Override - public void unbindOutputs(BindingService bindingService) { - for (Map.Entry boundTargetHolderEntry : this.outputHolders - .entrySet()) { - if (boundTargetHolderEntry.getValue().isBindable()) { - bindingService.unbindProducers(boundTargetHolderEntry.getKey()); - } - } - } - - @Override - public Set getInputs() { - return this.inputHolders.keySet(); - } - - @Override - public Set getOutputs() { - return this.outputHolders.keySet(); - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindingLifecycle.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindingLifecycle.java deleted file mode 100644 index 2cf9219ad..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindingLifecycle.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binding; - -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.SmartLifecycle; - -/** - * Base implementation of lifecycle operations for {@link BindingService} aware - * {@link Bindable}s. - * - * @author Oleg Zhurakousky - * @see InputBindingLifecycle - * @see OutputBindingLifecycle - */ -abstract class AbstractBindingLifecycle implements SmartLifecycle { - - final BindingService bindingService; - - private final Map bindables; - - @Autowired - private ApplicationContext context; - - private volatile boolean running; - - AbstractBindingLifecycle(BindingService bindingService, - Map bindables) { - this.bindingService = bindingService; - this.bindables = bindables; - } - - @Override - public void start() { - if (!this.running) { - if (this.context != null) { - this.bindables.putAll(context.getBeansOfType(Bindable.class)); - } - - this.bindables.values().forEach(this::doStartWithBindable); - this.running = true; - } - } - - @Override - public void stop() { - if (this.running) { - if (this.context != null) { - this.bindables.putAll(context.getBeansOfType(Bindable.class)); - } - this.bindables.values().forEach(this::doStopWithBindable); - this.running = false; - } - } - - @Override - public boolean isRunning() { - return this.running; - } - - @Override - public boolean isAutoStartup() { - return true; - } - - @Override - public void stop(Runnable callback) { - stop(); - if (callback != null) { - callback.run(); - } - } - - abstract void doStartWithBindable(Bindable bindable); - - abstract void doStopWithBindable(Bindable bindable); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindingTargetFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindingTargetFactory.java deleted file mode 100644 index a222240b6..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/AbstractBindingTargetFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binding; - -import org.springframework.util.Assert; - -/** - * A {@link BindingTargetFactory} implementation that restricts the type of binding target - * to a specified class and its supertypes. - * - * @param type of binding target - * @author Marius Bogoevici - */ -public abstract class AbstractBindingTargetFactory implements BindingTargetFactory { - - private final Class bindingTargetType; - - protected AbstractBindingTargetFactory(Class bindingTargetType) { - Assert.notNull(bindingTargetType, "The binding target type cannot be null"); - this.bindingTargetType = bindingTargetType; - } - - @Override - public final boolean canCreate(Class clazz) { - return clazz.isAssignableFrom(this.bindingTargetType); - } - - @Override - public abstract T createInput(String name); - - @Override - public abstract T createOutput(String name); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/Bindable.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/Bindable.java deleted file mode 100644 index dcd3438db..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/Bindable.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.util.Collection; -import java.util.Collections; -import java.util.Set; - -import org.springframework.cloud.stream.binder.Binding; - -/** - * Marker interface for instances that can bind/unbind groups of inputs and outputs. - * - * Intended for internal use. - * - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public interface Bindable { - - /** - * Binds all the inputs associated with this instance. - * @param adapter instance of {@link BindingService} - * @return collection of {@link Binding}s - * - * @since 2.0 - */ - default Collection> createAndBindInputs(BindingService adapter) { - return Collections.>emptyList(); - } - - /** - * Binds all the outputs associated with this instance. - * @param adapter instance of {@link BindingService} - * @return collection of {@link Binding}s - * - * @since 2.0 - */ - default Collection> createAndBindOutputs(BindingService adapter) { - return Collections.>emptyList(); - } - - /** - * Unbinds all the inputs associated with this instance. - * @param adapter binding service - */ - default void unbindInputs(BindingService adapter) { - } - - /** - * Unbinds all the outputs associated with this instance. - * @param adapter binding service - */ - default void unbindOutputs(BindingService adapter) { - } - - /** - * Enumerates all the input binding names. - * @return input binding names - */ - default Set getInputs() { - return Collections.emptySet(); - } - - /** - * Enumerates all the output binding names. - * @return output binding names - */ - default Set getOutputs() { - return Collections.emptySet(); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindableProxyFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindableProxyFactory.java deleted file mode 100644 index d6b6958e3..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindableProxyFactory.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binding; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.util.Assert; - -/** - * {@link FactoryBean} for instantiating the functions. - * - * @author Marius Bogoevici - * @author David Syer - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - * @author Soby Chacko - */ -public class BindableProxyFactory extends AbstractBindableProxyFactory - implements MethodInterceptor, FactoryBean, InitializingBean, BeanFactoryAware { - - private static Log log = LogFactory.getLog(BindableProxyFactory.class); - - private final Map targetCache = new HashMap<>(2); - - private Object proxy; - - protected BeanFactory beanFactory; - - public BindableProxyFactory(Class type) { - super(type); - this.type = type; - } - - @Override - public synchronized Object invoke(MethodInvocation invocation) { - Method method = invocation.getMethod(); - - // try to use cached target - Object boundTarget = this.targetCache.get(method); - if (boundTarget != null) { - return boundTarget; - } - return null; - } - - public void replaceInputChannel(String originalChannelName, String newChannelName, SubscribableChannel messageChannel) { - if (log.isInfoEnabled()) { - log.info("Replacing '" + originalChannelName + "' binding channel with '" + newChannelName + "'"); - } - BoundTargetHolder holder = new BoundTargetHolder(messageChannel, true); - this.inputHolders.remove(originalChannelName); - this.inputHolders.put(newChannelName, holder); - } - - public void replaceOutputChannel(String originalChannelName, String newChannelName, MessageChannel messageChannel) { - if (log.isInfoEnabled()) { - log.info("Replacing '" + originalChannelName + "' binding channel with '" + newChannelName + "'"); - } - BoundTargetHolder holder = new BoundTargetHolder(messageChannel, true); - this.outputHolders.remove(originalChannelName); - this.outputHolders.put(newChannelName, holder); - } - - @Override - public void afterPropertiesSet() { - populateBindingTargetFactories(this.beanFactory); - Assert.notEmpty(BindableProxyFactory.this.bindingTargetFactories, - "'bindingTargetFactories' cannot be empty"); - } - - @Override - public synchronized Object getObject() { - if (this.proxy == null && this.type != null) { - ProxyFactory factory = new ProxyFactory(this.type, this); - this.proxy = factory.getProxy(); - } - return this.proxy; - } - - @Override - public Class getObjectType() { - return this.type; - } - - @Override - public boolean isSingleton() { - return true; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareRouter.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareRouter.java deleted file mode 100644 index e3b238044..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareRouter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2013-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.cloud.stream.binding; - -import org.springframework.integration.router.AbstractMappingMessageRouter; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.core.DestinationResolver; - -/** - * Sets a BinderAwareChannelResolver on any bean - * of type {@link AbstractMappingMessageRouter} within the context. - * - * @author Mark Fisher - * @author Gary Russell - * @author Oleg Zhurakousky - */ -public class BinderAwareRouter { - - public BinderAwareRouter(AbstractMappingMessageRouter[] routers, - DestinationResolver channelResolver) { - if (routers != null) { - for (AbstractMappingMessageRouter router : routers) { - router.setChannelResolver(channelResolver); - } - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingService.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingService.java deleted file mode 100644 index 8abc02922..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingService.java +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.framework.Advised; -import org.springframework.beans.BeanUtils; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedConsumerProperties; -import org.springframework.cloud.stream.binder.ExtendedProducerProperties; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.cloud.stream.binder.PollableConsumerBinder; -import org.springframework.cloud.stream.binder.PollableSource; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.lang.Nullable; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; -import org.springframework.validation.DataBinder; - -/** - * Handles binding of input/output targets by delegating to an underlying {@link Binder}. - * - * @author Mark Fisher - * @author Dave Syer - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Janne Valkealahti - * @author Soby Chacko - * @author Michael Michailidis - * @author Chris Bono - * @author Artem Bilan - * @author Byungjun You - */ -public class BindingService { - -// private final CustomValidatorBean validator; - - private final Log log = LogFactory.getLog(BindingService.class); - - private final BindingServiceProperties bindingServiceProperties; - - private final Map> producerBindings = new HashMap<>(); - - private final Map>> consumerBindings = new HashMap<>(); - - private final TaskScheduler taskScheduler; - - private final BinderFactory binderFactory; - - private final ObjectMapper objectMapper; - - public BindingService(BindingServiceProperties bindingServiceProperties, - BinderFactory binderFactory, ObjectMapper objectMapper) { - this(bindingServiceProperties, binderFactory, null, objectMapper); - } - - public BindingService(BindingServiceProperties bindingServiceProperties, - BinderFactory binderFactory, TaskScheduler taskScheduler, ObjectMapper objectMapper) { - this.bindingServiceProperties = bindingServiceProperties; - this.binderFactory = binderFactory; -// this.validator = new CustomValidatorBean(); -// this.validator.afterPropertiesSet(); - this.taskScheduler = taskScheduler; - this.objectMapper = objectMapper; - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Collection> bindConsumer(T input, String inputName) { - Collection> bindings = new ArrayList<>(); - Class inputClass = input.getClass(); - if (input instanceof Advised advisedInput) { - inputClass = Stream.of(advisedInput.getProxiedInterfaces()).filter(c -> !c.getName().contains("org.springframework")).findFirst() - .orElse(inputClass); - } - Binder binder = (Binder) getBinder( - inputName, inputClass); - ConsumerProperties consumerProperties = this.bindingServiceProperties - .getConsumerProperties(inputName); - if (binder instanceof ExtendedPropertiesBinder extendedPropertiesBinder) { - Object extension = extendedPropertiesBinder - .getExtendedConsumerProperties(inputName); - ExtendedConsumerProperties extendedConsumerProperties = new ExtendedConsumerProperties( - extension); - BeanUtils.copyProperties(consumerProperties, extendedConsumerProperties); - - consumerProperties = extendedConsumerProperties; - } - consumerProperties.populateBindingName(inputName); - validate(consumerProperties); - - String bindingTarget = this.bindingServiceProperties - .getBindingDestination(inputName); - - if (consumerProperties.isMultiplex()) { - bindings.add(doBindConsumer(input, inputName, binder, consumerProperties, - bindingTarget)); - } - else { - String[] bindingTargets = StringUtils - .commaDelimitedListToStringArray(bindingTarget); - for (String target : bindingTargets) { - if (!consumerProperties.isPartitioned() || consumerProperties.getInstanceIndexList().isEmpty()) { - Binding binding = input instanceof PollableSource - ? doBindPollableConsumer(input, inputName, binder, - consumerProperties, target) - : doBindConsumer(input, inputName, binder, consumerProperties, - target); - - bindings.add(binding); - } - else { - for (Integer index : consumerProperties.getInstanceIndexList()) { - if (index < 0) { - continue; - } - - Object extension = consumerProperties instanceof ExtendedConsumerProperties extendedProperties - ? extendedProperties.getExtension() - : null; - - ConsumerProperties consumerPropertiesTemp = new ExtendedConsumerProperties<>(extension); - BeanUtils.copyProperties(consumerProperties, consumerPropertiesTemp); - - consumerPropertiesTemp.setInstanceIndex(index); - - Binding binding = input instanceof PollableSource - ? doBindPollableConsumer(input, inputName, binder, - consumerPropertiesTemp, target) - : doBindConsumer(input, inputName, binder, consumerPropertiesTemp, - target); - - bindings.add(binding); - } - } - } - } - bindings = Collections.unmodifiableCollection(bindings); - this.consumerBindings.put(inputName, new ArrayList<>(bindings)); - return bindings; - } - - public Binding doBindConsumer(T input, String inputName, - Binder binder, - ConsumerProperties consumerProperties, String target) { - if (this.taskScheduler == null - || this.bindingServiceProperties.getBindingRetryInterval() <= 0) { - return binder.bindConsumer(target, - this.bindingServiceProperties.getGroup(inputName), input, - consumerProperties); - } - else { - try { - return binder.bindConsumer(target, - this.bindingServiceProperties.getGroup(inputName), input, - consumerProperties); - } - catch (RuntimeException e) { - LateBinding late = new LateBinding(target, - e.getCause() == null ? e.toString() : e.getCause().getMessage(), consumerProperties, true, this.objectMapper); - rescheduleConsumerBinding(input, inputName, binder, consumerProperties, - target, late, e); - this.consumerBindings.put(inputName, Collections.singletonList(late)); - return late; - } - } - } - - public void rescheduleConsumerBinding(final T input, final String inputName, - final Binder binder, - final ConsumerProperties consumerProperties, final String target, - final LateBinding late, RuntimeException exception) { - assertNotIllegalException(exception); - this.log.error("Failed to create consumer binding; retrying in " - + this.bindingServiceProperties.getBindingRetryInterval() + " seconds", - exception); - this.scheduleTask(() -> { - try { - late.setDelegate(binder.bindConsumer(target, - this.bindingServiceProperties.getGroup(inputName), input, - consumerProperties)); - } - catch (RuntimeException e) { - rescheduleConsumerBinding(input, inputName, binder, consumerProperties, - target, late, e); - } - }); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Binding doBindPollableConsumer(T input, String inputName, - Binder binder, - ConsumerProperties consumerProperties, String target) { - if (this.taskScheduler == null - || this.bindingServiceProperties.getBindingRetryInterval() <= 0) { - return ((PollableConsumerBinder) binder).bindPollableConsumer(target, - this.bindingServiceProperties.getGroup(inputName), - (PollableSource) input, consumerProperties); - } - else { - try { - return ((PollableConsumerBinder) binder).bindPollableConsumer(target, - this.bindingServiceProperties.getGroup(inputName), - (PollableSource) input, consumerProperties); - } - catch (RuntimeException e) { - LateBinding late = new LateBinding(target, - e.getCause() == null ? e.toString() : e.getCause().getMessage(), consumerProperties, true, this.objectMapper); - reschedulePollableConsumerBinding(input, inputName, binder, - consumerProperties, target, late, e); - return late; - } - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void reschedulePollableConsumerBinding(final T input, - final String inputName, final Binder binder, - final ConsumerProperties consumerProperties, final String target, - final LateBinding late, RuntimeException exception) { - assertNotIllegalException(exception); - this.log.error("Failed to create consumer binding; retrying in " - + this.bindingServiceProperties.getBindingRetryInterval() + " seconds", - exception); - this.scheduleTask(() -> { - try { - late.setDelegate(((PollableConsumerBinder) binder).bindPollableConsumer( - target, this.bindingServiceProperties.getGroup(inputName), - (PollableSource) input, consumerProperties)); - } - catch (RuntimeException e) { - reschedulePollableConsumerBinding(input, inputName, binder, - consumerProperties, target, late, e); - } - }); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Binding bindProducer(T output, String outputName, boolean cache, @Nullable Binder binder) { - String bindingTarget = this.bindingServiceProperties.getBindingDestination(outputName); - Class outputClass = output.getClass(); - if (output instanceof Advised advisedOutput) { - outputClass = Stream.of(advisedOutput.getProxiedInterfaces()).filter(c -> !c.getName().contains("org.springframework")).findFirst() - .orElse(outputClass); - } - if (binder == null) { - binder = (Binder) getBinder(outputName, outputClass); - } - - ProducerProperties producerProperties = this.bindingServiceProperties - .getProducerProperties(outputName); - if (binder instanceof ExtendedPropertiesBinder extendedPropertiesBinder) { - Object extension = extendedPropertiesBinder.getExtendedProducerProperties(outputName); - ExtendedProducerProperties extendedProducerProperties = new ExtendedProducerProperties<>( - extension); - BeanUtils.copyProperties(producerProperties, extendedProducerProperties); - - producerProperties = extendedProducerProperties; - } - producerProperties.populateBindingName(outputName); - validate(producerProperties); - Binding binding = doBindProducer(output, bindingTarget, binder, - producerProperties); - if (cache) { - this.producerBindings.put(outputName, binding); - } - return binding; - } - - public Binding bindProducer(T output, String outputName, boolean cache) { - return this.bindProducer(output, outputName, cache, null); - } - - public Binding bindProducer(T output, String outputName) { - return this.bindProducer(output, outputName, true); - } - - @SuppressWarnings("rawtypes") - public Object getExtendedProducerProperties(Object output, String outputName) { - Binder binder = getBinder(outputName, output.getClass()); - if (binder instanceof ExtendedPropertiesBinder extendedPropertiesBinder) { - return extendedPropertiesBinder.getExtendedProducerProperties(outputName); - } - return null; - } - - public String[] getProducerBindingNames() { - return this.producerBindings.keySet().toArray(new String[] {}); - } - - @Nullable - public Binding getProducerBinding(String bindingName) { - return this.producerBindings.get(bindingName); - } - - public String[] getConsumerBindingNames() { - return this.consumerBindings.keySet().toArray(new String[] {}); - } - - public List> getConsumerBindings(String bindingName) { - return this.consumerBindings.getOrDefault(bindingName, Collections.emptyList()); - } - - public Binding doBindProducer(T output, String bindingTarget, - Binder binder, - ProducerProperties producerProperties) { - if (this.taskScheduler == null - || this.bindingServiceProperties.getBindingRetryInterval() <= 0) { - return binder.bindProducer(bindingTarget, output, producerProperties); - } - else { - try { - return binder.bindProducer(bindingTarget, output, producerProperties); - } - catch (RuntimeException e) { - LateBinding late = new LateBinding(bindingTarget, - e.getCause() == null ? e.toString() : e.getCause().getMessage(), producerProperties, false, this.objectMapper); - rescheduleProducerBinding(output, bindingTarget, binder, - producerProperties, late, e); - return late; - } - } - } - - public void rescheduleProducerBinding(final T output, final String bindingTarget, - final Binder binder, - final ProducerProperties producerProperties, final LateBinding late, - final RuntimeException exception) { - assertNotIllegalException(exception); - this.log.error("Failed to create producer binding; retrying in " - + this.bindingServiceProperties.getBindingRetryInterval() + " seconds", - exception); - this.scheduleTask(() -> { - try { - late.setDelegate( - binder.bindProducer(bindingTarget, output, producerProperties)); - } - catch (RuntimeException e) { - rescheduleProducerBinding(output, bindingTarget, binder, - producerProperties, late, e); - } - }); - } - - public void unbindConsumers(String inputName) { - List> bindings = this.consumerBindings.remove(inputName); - if (bindings != null && !CollectionUtils.isEmpty(bindings)) { - for (Binding binding : bindings) { - binding.stop(); - //then - binding.unbind(); - } - } - else if (this.log.isWarnEnabled()) { - this.log.warn("Trying to unbind '" + inputName + "', but no binding found."); - } - } - - public void unbindProducers(String outputName) { - Binding binding = this.producerBindings.remove(outputName); - - if (binding != null) { - binding.stop(); - //then - binding.unbind(); - } - else if (this.log.isWarnEnabled()) { - this.log.warn("Trying to unbind '" + outputName + "', but no binding found."); - } - } - - public BindingServiceProperties getBindingServiceProperties() { - return this.bindingServiceProperties; - } - - protected Binder getBinder(String channelName, Class bindableType) { - String binderConfigurationName = this.bindingServiceProperties - .getBinder(channelName); - return this.binderFactory.getBinder(binderConfigurationName, bindableType); - } - - private void validate(Object properties) { - DataBinder dataBinder = new DataBinder(properties); -// dataBinder.setValidator(this.validator); - dataBinder.validate(); - if (dataBinder.getBindingResult().hasErrors()) { - throw new IllegalStateException(dataBinder.getBindingResult().toString()); - } - } - - private void scheduleTask(Runnable task) { - this.taskScheduler.schedule(task, Instant.ofEpochMilli(System.currentTimeMillis() - + this.bindingServiceProperties.getBindingRetryInterval() * 1_000)); - } - - private void assertNotIllegalException(RuntimeException exception) - throws RuntimeException { - if (exception instanceof IllegalStateException - || exception instanceof IllegalArgumentException) { - throw exception; - } - } - - public static class LateBinding implements Binding { - - private volatile Binding delegate; - - private volatile boolean unbound; - - private final String error; - - private final String bindingName; - - private final Object consumerOrProducerproperties; - - private final boolean isInput; - - final ObjectMapper objectMapper; - - LateBinding(String bindingName, String error, Object consumerOrProducerproperties, boolean isInput, ObjectMapper objectMapper) { - super(); - this.error = error; - this.bindingName = bindingName; - this.consumerOrProducerproperties = consumerOrProducerproperties; - this.isInput = isInput; - this.objectMapper = objectMapper; - } - - public synchronized void setDelegate(Binding delegate) { - if (this.unbound) { - delegate.unbind(); - } - else { - this.delegate = delegate; - } - } - - @Override - public synchronized void unbind() { - this.unbound = true; - if (this.delegate != null) { - this.delegate.unbind(); - } - } - - @Override - public String getName() { - return this.bindingName; - } - - @Override - public String getBindingName() { - return this.bindingName; - } - - @SuppressWarnings("unused") - public String getError() { - return this.error; - } - - @Override - public String toString() { - return "LateBinding [delegate=" + this.delegate + "]"; - } - - @Override - public Map getExtendedInfo() { - Map extendedInfo = new LinkedHashMap<>(); - extendedInfo.put("bindingDestination", this.getBindingName()); - extendedInfo.put(consumerOrProducerproperties.getClass().getSimpleName(), - this.objectMapper.convertValue(consumerOrProducerproperties, Map.class)); - return extendedInfo; - } - - @Override - public boolean isInput() { - return this.isInput; - } - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingTargetFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingTargetFactory.java deleted file mode 100644 index 43d16c8a4..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingTargetFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binding; - -/** - * Defines methods to create/configure the binding targets defined by - * {@link org.springframework.cloud.stream.annotation.EnableBinding}. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - */ -public interface BindingTargetFactory { - - /** - * Checks whether a specific binding target type can be created by this factory. - * @param clazz the binding target type - * @return true if the binding target can be created - */ - boolean canCreate(Class clazz); - - /** - * Create an input binding target that will be bound via a corresponding - * {@link org.springframework.cloud.stream.binder.Binder}. - * @param name name of the binding target - * @return binding target - */ - Object createInput(String name); - - /** - * Create an output binding target that will be bound via a corresponding - * {@link org.springframework.cloud.stream.binder.Binder}. - * @param name name of the binding target - * @return binding target - */ - Object createOutput(String name); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingsLifecycleController.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingsLifecycleController.java deleted file mode 100644 index cc85c92f2..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BindingsLifecycleController.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * - * Lifecycle controller for the bindings. - * It is registered as a bean and once injected could be used to control the lifecycle f the bindings. - * - * @author Oleg Zhurakousky - * @since 3.x - */ -public class BindingsLifecycleController { - - private final List inputBindingLifecycles; - - private final List outputBindingsLifecycles; - - private final ObjectMapper objectMapper; - - @SuppressWarnings("unchecked") - public BindingsLifecycleController(List inputBindingLifecycles, - List outputBindingsLifecycles) { - Assert.notEmpty(inputBindingLifecycles, - "'inputBindingLifecycles' must not be null or empty"); - this.inputBindingLifecycles = inputBindingLifecycles; - this.outputBindingsLifecycles = outputBindingsLifecycles; - - - this.objectMapper = new ObjectMapper(); //see https://github.com/spring-cloud/spring-cloud-stream/issues/2253 - // we need to use ObjectMapper that could not be modified by the user. - - try { - Class javaTimeModuleClass = (Class) - ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", ClassUtils.getDefaultClassLoader()); - Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass); - this.objectMapper.registerModule(javaTimeModule); - } - catch (ClassNotFoundException ex) { - // ignore; jackson-datatype-jsr310 not available - } - } - - /** - * Convenience method to stop the binding with provided `bindingName`. - * @param bindingName the name of the binding. - */ - public void stop(String bindingName) { - this.changeState(bindingName, State.STOPPED); - } - - /** - * Convenience method to start the binding with provided `bindingName`. - * @param bindingName the name of the binding. - */ - public void start(String bindingName) { - this.changeState(bindingName, State.STARTED); - } - - /** - * Convenience method to pause the binding with provided `bindingName`. - * @param bindingName the name of the binding. - */ - public void pause(String bindingName) { - this.changeState(bindingName, State.PAUSED); - } - - /** - * Convenience method to resume the binding with provided `bindingName`. - * @param bindingName the name of the binding. - */ - public void resume(String bindingName) { - this.changeState(bindingName, State.RESUMED); - } - - /** - * General purpose method to change the state of the provided binding. - * @param bindingName the name of the binding. - * @param state the {@link State} you wish to set this binding to - */ - public void changeState(String bindingName, State state) { - var bindingList = BindingsLifecycleController.this.locateBinding(bindingName); - if (!bindingList.isEmpty()) { - switch (state) { - case STARTED -> bindingList.stream().forEach(Binding::start); - case STOPPED -> bindingList.stream().forEach(Binding::stop); - case PAUSED -> bindingList.stream().forEach(Binding::pause); - case RESUMED -> bindingList.stream().forEach(Binding::resume); - default -> { - } - } - } - } - - /** - * Queries the {@link List} of states for all available bindings. The returned list - * consists of {@link Binding} objects which could be further interrogated - * using {@link Binding#isPaused()} and {@link Binding#isRunning()}. - * @return the list of {@link Binding}s - */ - @SuppressWarnings("unchecked") - public List> queryStates() { - List> bindings = new ArrayList<>(gatherInputBindings()); - bindings.addAll(gatherOutputBindings()); - return this.objectMapper.convertValue(bindings, List.class); - } - - /** - * Queries the individual state of a binding. The returned list - * {@link Binding} object could be further interrogated - * using {@link Binding#isPaused()} and {@link Binding#isRunning()}. - * @return collection of {@link Binding} objects. - */ - public List> queryState(String name) { - Assert.notNull(name, "'name' must not be null"); - return this.locateBinding(name); - } - - /** - * Queries for all input {@link Binding}s. - * @return the list of input {@link Binding}s - */ - @SuppressWarnings("unchecked") - private List> gatherInputBindings() { - List> inputBindings = new ArrayList<>(); - for (InputBindingLifecycle inputBindingLifecycle : this.inputBindingLifecycles) { - Collection> lifecycleInputBindings = (Collection>) new DirectFieldAccessor( - inputBindingLifecycle).getPropertyValue("inputBindings"); - inputBindings.addAll(lifecycleInputBindings); - } - return inputBindings; - } - - /** - * Queries for all output {@link Binding}s. - * @return the list of output {@link Binding}s - */ - @SuppressWarnings("unchecked") - private List> gatherOutputBindings() { - List> outputBindings = new ArrayList<>(); - for (OutputBindingLifecycle inputBindingLifecycle : this.outputBindingsLifecycles) { - Collection> lifecycleInputBindings = (Collection>) new DirectFieldAccessor( - inputBindingLifecycle).getPropertyValue("outputBindings"); - outputBindings.addAll(lifecycleInputBindings); - } - return outputBindings; - } - - private List> locateBinding(String name) { - Stream> bindings = Stream.concat(this.gatherInputBindings().stream(), - this.gatherOutputBindings().stream()); - return bindings.filter(binding -> name.equals(binding.getBindingName())).collect(Collectors.toList()); - } - - /** - * Binding states. - */ - public enum State { - - /** - * Started state of a binding. - */ - STARTED, - - /** - * Stopped state of a binding. - */ - STOPPED, - - /** - * Paused state of a binding. - */ - PAUSED, - - /** - * Resumed state of a binding. - */ - RESUMED; - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BoundTargetHolder.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BoundTargetHolder.java deleted file mode 100644 index f3a384527..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BoundTargetHolder.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binding; - -/** - * Holds information about the binding targets exposed by the interface proxy, as well - * as their status. - * - * Refactored from {@link BindableProxyFactory}. - * - * @author Original authors in {@link BindableProxyFactory} - * @author Soby Chacko - * @since 3.0.0 - */ -public final class BoundTargetHolder { - - private Object boundTarget; - - private boolean bindable; - - public BoundTargetHolder(Object boundTarget, boolean bindable) { - this.boundTarget = boundTarget; - this.bindable = bindable; - } - - public Object getBoundTarget() { - return this.boundTarget; - } - - public boolean isBindable() { - return this.bindable; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/CompositeMessageChannelConfigurer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/CompositeMessageChannelConfigurer.java deleted file mode 100644 index b2edbfebe..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/CompositeMessageChannelConfigurer.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.util.List; - -import org.springframework.cloud.stream.binder.PollableMessageSource; -import org.springframework.messaging.MessageChannel; - -/** - * {@link MessageChannelConfigurer} that composes all the message channel configurers. - * - * @author Ilayaperumal Gopinathan - */ -public class CompositeMessageChannelConfigurer - implements MessageChannelAndSourceConfigurer { - - private final List messageChannelConfigurers; - - public CompositeMessageChannelConfigurer( - List messageChannelConfigurers) { - this.messageChannelConfigurers = messageChannelConfigurers; - } - - @Override - public void configureInputChannel(MessageChannel messageChannel, String channelName) { - for (MessageChannelConfigurer messageChannelConfigurer : this.messageChannelConfigurers) { - if (messageChannelConfigurer != null) { - messageChannelConfigurer.configureInputChannel(messageChannel, channelName); - } - - } - } - - @Override - public void configureOutputChannel(MessageChannel messageChannel, String channelName) { - for (MessageChannelConfigurer messageChannelConfigurer : this.messageChannelConfigurers) { - if (messageChannelConfigurer != null) { - messageChannelConfigurer.configureOutputChannel(messageChannel, channelName); - } - } - } - - @Override - public void configurePolledMessageSource(PollableMessageSource binding, String name) { - this.messageChannelConfigurers.forEach(cconfigurer -> { - if (cconfigurer instanceof MessageChannelAndSourceConfigurer) { - ((MessageChannelAndSourceConfigurer) cconfigurer) - .configurePolledMessageSource(binding, name); - } - }); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ContextStartAfterRefreshListener.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ContextStartAfterRefreshListener.java deleted file mode 100644 index 0785bd153..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ContextStartAfterRefreshListener.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binding; - -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.event.ContextRefreshedEvent; - -/** - * Automatically starts the context after a refresh. - * - * @author Marius Bogoevici - */ -public class ContextStartAfterRefreshListener - implements ApplicationListener, ApplicationContextAware { - - private ApplicationContext applicationContext; - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - this.applicationContext = applicationContext; - } - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - ConfigurableApplicationContext source = (ConfigurableApplicationContext) event - .getSource(); - if (source == this.applicationContext && !source.isRunning()) { - source.start(); - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/DefaultPartitioningInterceptor.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/DefaultPartitioningInterceptor.java deleted file mode 100644 index a0fdcde8b..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/DefaultPartitioningInterceptor.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2021-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.integration.expression.ExpressionUtils; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.support.ChannelInterceptor; - -/** - * - * @author Oleg Zhurakousky - * @since 3.1 - * - */ -public class DefaultPartitioningInterceptor implements ChannelInterceptor { - - private final PartitionHandler partitionHandler; - - public DefaultPartitioningInterceptor(BindingProperties bindingProperties, ConfigurableListableBeanFactory beanFactory) { - this.partitionHandler = new PartitionHandler( - ExpressionUtils.createStandardEvaluationContext(beanFactory), - bindingProperties.getProducer(), beanFactory); - } - - public void setPartitionCount(int partitionCount) { - this.partitionHandler.setPartitionCount(partitionCount); - } - - @Override - public Message preSend(Message message, MessageChannel channel) { - if (!message.getHeaders().containsKey(BinderHeaders.PARTITION_OVERRIDE)) { - int partition = this.partitionHandler.determinePartition(message); - return MessageBuilder - .fromMessage(message) - .setHeader(BinderHeaders.PARTITION_HEADER, partition).build(); - } - else { - return MessageBuilder - .fromMessage(message) - .setHeader(BinderHeaders.PARTITION_HEADER, - message.getHeaders() - .get(BinderHeaders.PARTITION_OVERRIDE)) - .removeHeader(BinderHeaders.PARTITION_OVERRIDE).build(); - } - } - -} - diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/DynamicDestinationsBindable.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/DynamicDestinationsBindable.java deleted file mode 100644 index b0658ab46..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/DynamicDestinationsBindable.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.binding; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.springframework.cloud.stream.binder.Binding; - -/** - * A {@link Bindable} that stores the dynamic destination names and handles their - * unbinding. - * - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - */ -public final class DynamicDestinationsBindable implements Bindable { - - /** - * Map containing dynamic channel names and their bindings. - */ - private final Map> outputBindings = new HashMap<>(); - - public synchronized void addOutputBinding(String name, Binding binding) { - this.outputBindings.put(name, binding); - } - - @Override - public synchronized Set getOutputs() { - return Collections.unmodifiableSet(this.outputBindings.keySet()); - } - - @Override - public synchronized void unbindOutputs(BindingService adapter) { - for (Map.Entry> entry : this.outputBindings.entrySet()) { - entry.getValue().unbind(); - } - this.outputBindings.clear(); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/FluxMessageChannelBindingTargetFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/FluxMessageChannelBindingTargetFactory.java deleted file mode 100644 index 28157cc71..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/FluxMessageChannelBindingTargetFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binding; - -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.util.Assert; - -/** - * @author Soby Chacko - * @since 4.0.0 - */ -public class FluxMessageChannelBindingTargetFactory extends AbstractBindingTargetFactory { - - private final MessageChannelConfigurer messageChannelConfigurer; - - private final GenericApplicationContext context; - - public FluxMessageChannelBindingTargetFactory(MessageChannelConfigurer messageChannelConfigurer, - GenericApplicationContext context) { - super(FluxMessageChannel.class); - Assert.notNull(context, "'context' must not be null"); - this.messageChannelConfigurer = messageChannelConfigurer; - this.context = context; - } - - @Override - public FluxMessageChannel createInput(String name) { - FluxMessageChannel fluxMessageChannel = fluxMessageChannel(name); - this.messageChannelConfigurer.configureInputChannel(fluxMessageChannel, name); - return fluxMessageChannel; - } - - @Override - public FluxMessageChannel createOutput(String name) { - FluxMessageChannel fluxMessageChannel = fluxMessageChannel(name); - this.messageChannelConfigurer.configureOutputChannel(fluxMessageChannel, name); - return fluxMessageChannel; - } - - public FluxMessageChannel fluxMessageChannel(String name) { - FluxMessageChannel fluxMessageChannel = null; - if (context.containsBean(name)) { - fluxMessageChannel = context.getBean(name, FluxMessageChannel.class); - } - if (fluxMessageChannel == null) { - FluxMessageChannel channel = new FluxMessageChannel(); - channel.setComponentName(name); - context.registerBean(name, FluxMessageChannel.class, () -> channel); - fluxMessageChannel = channel; - } - return fluxMessageChannel; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/InputBindingLifecycle.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/InputBindingLifecycle.java deleted file mode 100644 index 1055dd358..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/InputBindingLifecycle.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; - -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.util.CollectionUtils; - -/** - * Coordinates binding/unbinding of input binding targets in accordance to the lifecycle - * of the host context. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - */ -public class InputBindingLifecycle extends AbstractBindingLifecycle { - - @SuppressWarnings("unused") - // It is actually used reflectively since at the moment we do not want to expose it - // via public method - private Collection> inputBindings = new ArrayList>(); - - public InputBindingLifecycle(BindingService bindingService, - Map bindables) { - super(bindingService, bindables); - } - - /** - * Return a high value so that this bean is started after receiving Lifecycle beans - * are started. Beans that need to start after bindings will set a higher phase value. - */ - @Override - public int getPhase() { - return Integer.MAX_VALUE - 1000; - } - - @Override - void doStartWithBindable(Bindable bindable) { - Collection> bindableBindings = bindable - .createAndBindInputs(this.bindingService); - if (!CollectionUtils.isEmpty(bindableBindings)) { - this.inputBindings.addAll(bindableBindings); - } - } - - @Override - void doStopWithBindable(Bindable bindable) { - bindable.unbindInputs(this.bindingService); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageChannelAndSourceConfigurer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageChannelAndSourceConfigurer.java deleted file mode 100644 index d88bd03a1..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageChannelAndSourceConfigurer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binding; - -import org.springframework.cloud.stream.binder.PollableMessageSource; - -/** - * Configurer for {@link PollableMessageSource}. - * - * @author Gary Russell - * @since 2.0 - * - */ -public interface MessageChannelAndSourceConfigurer extends MessageChannelConfigurer { - - /** - * Configure the provided message source binding. - * @param binding the binding. - * @param name the name. - */ - void configurePolledMessageSource(PollableMessageSource binding, String name); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageChannelConfigurer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageChannelConfigurer.java deleted file mode 100644 index 0fa3c2698..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageChannelConfigurer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binding; - -import org.springframework.messaging.MessageChannel; - -/** - * Interface to be implemented by the classes that configure the {@link Bindable} message - * channels. - * - * @author Ilayaperumal Gopinathan - */ -public interface MessageChannelConfigurer { - - /** - * Configure the given input message channel. - * @param messageChannel the message channel - * @param channelName name of the message channel - */ - void configureInputChannel(MessageChannel messageChannel, String channelName); - - /** - * Configure the given output message channel. - * @param messageChannel the message channel - * @param channelName name of the message channel - */ - void configureOutputChannel(MessageChannel messageChannel, String channelName); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageConverterConfigurer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageConverterConfigurer.java deleted file mode 100644 index 8c839c72c..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageConverterConfigurer.java +++ /dev/null @@ -1,362 +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.cloud.stream.binding; - -import java.lang.reflect.Field; -import java.util.Map; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.JavaClassMimeTypeUtils; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.binder.PollableMessageSource; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.converter.MessageConverterUtils; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.core.env.Environment; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.expression.ExpressionUtils; -import org.springframework.integration.support.MessageBuilderFactory; -import org.springframework.integration.support.MutableMessageBuilderFactory; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.util.Assert; -import org.springframework.util.MimeType; -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * A {@link MessageChannelConfigurer} that sets data types and message converters based on - * {@link org.springframework.cloud.stream.config.BindingProperties#contentType}. Also - * adds a {@link org.springframework.messaging.support.ChannelInterceptor} to the message - * channel to set the `ContentType` header for the message (if not already set) based on - * the `ContentType` binding property of the channel. - * - * @author Ilayaperumal Gopinathan - * @author Marius Bogoevici - * @author Maxim Kirilov - * @author Gary Russell - * @author Soby Chacko - * @author Oleg Zhurakousky - */ -public class MessageConverterConfigurer - implements MessageChannelAndSourceConfigurer, BeanFactoryAware { - - private final MessageBuilderFactory messageBuilderFactory = new MutableMessageBuilderFactory(); - - private final CompositeMessageConverter compositeMessageConverter; - - private final BindingServiceProperties bindingServiceProperties; - - private final Field headersField; - - private final StreamFunctionProperties streamFunctionProperties; - - private ConfigurableListableBeanFactory beanFactory; - - public MessageConverterConfigurer(BindingServiceProperties bindingServiceProperties, - CompositeMessageConverter compositeMessageConverter, StreamFunctionProperties streamFunctionProperties) { - Assert.notNull(compositeMessageConverter, - "The message converter factory cannot be null"); - this.bindingServiceProperties = bindingServiceProperties; - this.compositeMessageConverter = compositeMessageConverter; - - this.headersField = ReflectionUtils.findField(MessageHeaders.class, "headers"); - this.headersField.setAccessible(true); - this.streamFunctionProperties = streamFunctionProperties; - } - - public MessageConverterConfigurer(BindingServiceProperties bindingServiceProperties, - CompositeMessageConverter compositeMessageConverter) { - this(bindingServiceProperties, compositeMessageConverter, null); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - - @Override - public void configureInputChannel(MessageChannel messageChannel, String channelName) { - configureMessageChannel(messageChannel, channelName, true); - } - - @Override - public void configureOutputChannel(MessageChannel messageChannel, - String channelName) { - configureMessageChannel(messageChannel, channelName, false); - } - - @Override - public void configurePolledMessageSource(PollableMessageSource binding, String name) { - BindingProperties bindingProperties = this.bindingServiceProperties - .getBindingProperties(name); - String contentType = bindingProperties.getContentType(); - ConsumerProperties consumerProperties = bindingProperties.getConsumer(); - if ((consumerProperties == null || !consumerProperties.isUseNativeDecoding()) - && binding instanceof DefaultPollableMessageSource) { - ((DefaultPollableMessageSource) binding).addInterceptor( - new InboundContentTypeEnhancingInterceptor(contentType)); - } - } - - /** - * Setup data-type and message converters for the given message channel. - * @param channel message channel to set the data-type and message converters - * @param channelName the channel name - * @param inbound inbound (i.e., "input") or outbound channel - */ - private void configureMessageChannel(MessageChannel channel, String channelName, - boolean inbound) { - Assert.isAssignable(AbstractMessageChannel.class, channel.getClass()); - AbstractMessageChannel messageChannel = (AbstractMessageChannel) channel; - BindingProperties bindingProperties = this.bindingServiceProperties - .getBindingProperties(channelName); - String contentType = bindingProperties.getContentType(); - ProducerProperties producerProperties = bindingProperties.getProducer(); - boolean partitioned = !inbound && producerProperties != null && producerProperties.isPartitioned(); - boolean functional = streamFunctionProperties != null - && (StringUtils.hasText(streamFunctionProperties.getDefinition()) - || StringUtils.hasText(bindingServiceProperties.getInputBindings()) - || StringUtils.hasText(bindingServiceProperties.getOutputBindings())); - - if (partitioned) { - if (inbound || !functional) { - messageChannel.addInterceptor(new PartitioningInterceptor(bindingProperties)); - } - } - - Environment environment = this.beanFactory == null ? null : this.beanFactory.getBean(Environment.class); - ConsumerProperties consumerProperties = bindingProperties.getConsumer(); - if (this.isNativeEncodingNotSet(producerProperties, consumerProperties, inbound)) { - if (inbound) { - messageChannel.addInterceptor(new InboundContentTypeEnhancingInterceptor(contentType)); - } - else { - if (environment != null && environment.containsProperty("spring.cloud.stream.rabbit.bindings." + channelName + ".producer.routing-key-expression")) { - this.setSkipOutputConversionIfNecessary(); - functional = false; - } - if (!functional) { - messageChannel.addInterceptor(new OutboundContentTypeConvertingInterceptor(contentType, this.compositeMessageConverter)); - } - } - } - } - - private void setSkipOutputConversionIfNecessary() { - FunctionCatalog catalog = this.beanFactory.getBean(FunctionCatalog.class); - if (catalog != null) { - FunctionInvocationWrapper function = catalog.lookup(this.streamFunctionProperties.getDefinition()); - if (function != null) { - function.setSkipOutputConversion(true); - } - } - } - - private boolean isNativeEncodingNotSet(ProducerProperties producerProperties, - ConsumerProperties consumerProperties, boolean input) { - if (input) { - return consumerProperties == null - || !consumerProperties.isUseNativeDecoding(); - } - else { - return producerProperties == null - || !producerProperties.isUseNativeEncoding(); - } - } - - /** - * Primary purpose of this interceptor is to enhance/enrich Message that sent to the - * *inbound* channel with 'contentType' header for cases where 'contentType' is not - * present in the Message itself but set on such channel via - * {@link BindingProperties#setContentType(String)}.
- * Secondary purpose of this interceptor is to provide backward compatibility with - * previous versions of SCSt. - */ - private final class InboundContentTypeEnhancingInterceptor - extends AbstractContentTypeInterceptor { - - private InboundContentTypeEnhancingInterceptor(String contentType) { - super(contentType); - } - - @Override - public Message doPreSend(Message message, MessageChannel channel) { - @SuppressWarnings("unchecked") - Map headersMap = (Map) ReflectionUtils - .getField(MessageConverterConfigurer.this.headersField, - message.getHeaders()); - MimeType contentType = this.mimeType; - - - if (!message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) { - headersMap.put(MessageHeaders.CONTENT_TYPE, contentType); - } - else if (message.getHeaders() - .get(MessageHeaders.CONTENT_TYPE) instanceof String) { - headersMap.put(MessageHeaders.CONTENT_TYPE, MimeType.valueOf( - (String) message.getHeaders().get(MessageHeaders.CONTENT_TYPE))); - } - - return message; - } - - } - - /** - * Unlike INBOUND where the target type is known and conversion is typically done by - * argument resolvers of {@link InvocableHandlerMethod} for the OUTBOUND case it is - * not known so we simply rely on provided MessageConverters that will use the - * provided 'contentType' and convert messages to a type dictated by the Binders - * (i.e., byte[]). - */ - private final class OutboundContentTypeConvertingInterceptor - extends AbstractContentTypeInterceptor { - - private final MessageConverter messageConverter; - - private OutboundContentTypeConvertingInterceptor(String contentType, - CompositeMessageConverter messageConverter) { - super(contentType); - this.messageConverter = messageConverter; - } - - @Override - public Message doPreSend(Message message, MessageChannel channel) { - if (message.getPayload() instanceof byte[] - && message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) { - return message; - } - - String oct = message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE) - ? message.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString() - : null; - String ct = message.getPayload() instanceof String - ? JavaClassMimeTypeUtils.mimeTypeFromObject(message.getPayload(), - ObjectUtils.nullSafeToString(oct)).toString() - : oct; - - if (!message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) { - @SuppressWarnings("unchecked") - Map headersMap = (Map) ReflectionUtils - .getField(MessageConverterConfigurer.this.headersField, - message.getHeaders()); - headersMap.put(MessageHeaders.CONTENT_TYPE, this.mimeType); - } - - @SuppressWarnings("unchecked") - Message outboundMessage = message.getPayload() instanceof byte[] - ? (Message) message : (Message) this.messageConverter - .toMessage(message.getPayload(), message.getHeaders()); - if (outboundMessage == null) { - throw new IllegalStateException("Failed to convert message: '" + message - + "' to outbound message."); - } - - if (ct != null && !ct.equals(oct) && oct != null) { - @SuppressWarnings("unchecked") - Map headersMap = (Map) ReflectionUtils - .getField(MessageConverterConfigurer.this.headersField, - outboundMessage.getHeaders()); - headersMap.put(MessageHeaders.CONTENT_TYPE, MimeType.valueOf(ct)); - } - return outboundMessage; - } - - } - - /** - * - */ - private abstract class AbstractContentTypeInterceptor implements ChannelInterceptor { - - final MimeType mimeType; - - private AbstractContentTypeInterceptor(String contentType) { - this.mimeType = MessageConverterUtils.getMimeType(contentType); - } - - @Override - public Message preSend(Message message, MessageChannel channel) { - return message instanceof ErrorMessage ? message - : this.doPreSend(message, channel); - } - - protected abstract Message doPreSend(Message message, - MessageChannel channel); - - } - - /** - * Partitioning channel interceptor. - */ - public final class PartitioningInterceptor implements ChannelInterceptor { - - private final BindingProperties bindingProperties; - - private final PartitionHandler partitionHandler; - - PartitioningInterceptor(BindingProperties bindingProperties) { - this.bindingProperties = bindingProperties; - this.partitionHandler = new PartitionHandler( - ExpressionUtils.createStandardEvaluationContext( - MessageConverterConfigurer.this.beanFactory), - this.bindingProperties.getProducer(), MessageConverterConfigurer.this.beanFactory); - } - - public void setPartitionCount(int partitionCount) { - this.partitionHandler.setPartitionCount(partitionCount); - } - - @Override - public Message preSend(Message message, MessageChannel channel) { - if (!message.getHeaders().containsKey(BinderHeaders.PARTITION_OVERRIDE)) { - int partition = this.partitionHandler.determinePartition(message); - return MessageConverterConfigurer.this.messageBuilderFactory - .fromMessage(message) - .setHeader(BinderHeaders.PARTITION_HEADER, partition).build(); - } - else { - return MessageConverterConfigurer.this.messageBuilderFactory - .fromMessage(message) - .setHeader(BinderHeaders.PARTITION_HEADER, - message.getHeaders() - .get(BinderHeaders.PARTITION_OVERRIDE)) - .removeHeader(BinderHeaders.PARTITION_OVERRIDE).build(); - } - } - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageSourceBindingTargetFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageSourceBindingTargetFactory.java deleted file mode 100644 index 2524d663f..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/MessageSourceBindingTargetFactory.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binding; - -import org.springframework.cloud.stream.binder.DefaultPollableMessageSource; -import org.springframework.cloud.stream.binder.PollableMessageSource; -import org.springframework.messaging.converter.SmartMessageConverter; -import org.springframework.util.Assert; - -/** - * An implementation of {@link BindingTargetFactory} for creating - * {@link DefaultPollableMessageSource}s. - * - * @author Gary Russell - */ -public class MessageSourceBindingTargetFactory - extends AbstractBindingTargetFactory { - - private final MessageChannelAndSourceConfigurer messageSourceConfigurer; - - private final SmartMessageConverter messageConverter; - - public MessageSourceBindingTargetFactory(SmartMessageConverter messageConverter, - MessageChannelConfigurer messageSourceConfigurer) { - super(PollableMessageSource.class); - Assert.isInstanceOf(MessageChannelAndSourceConfigurer.class, - messageSourceConfigurer); - this.messageSourceConfigurer = (MessageChannelAndSourceConfigurer) messageSourceConfigurer; - this.messageConverter = messageConverter; - } - - @Override - public PollableMessageSource createInput(String name) { - DefaultPollableMessageSource binding = new DefaultPollableMessageSource( - this.messageConverter); - this.messageSourceConfigurer.configurePolledMessageSource(binding, name); - return binding; - } - - @Override - public PollableMessageSource createOutput(String name) { - throw new UnsupportedOperationException(); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/NewDestinationBindingCallback.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/NewDestinationBindingCallback.java deleted file mode 100644 index 0dc71f575..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/NewDestinationBindingCallback.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binding; - -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.messaging.MessageChannel; - -/** - * Configure a new destination before it is bound. - * - * @param the extended properties type. If you need to support dynamic binding - * with multiple binders, use {@link Object} and cast as needed. - * @since 2.0 - * - */ -@FunctionalInterface -public interface NewDestinationBindingCallback { - - /** - * Configure the properties or channel before binding. - * @param channelName the name of the new channel. - * @param channel the channel that is about to be bound. - * @param producerProperties the producer properties. - * @param extendedProducerProperties the extended producer properties (type - * depends on binder type and may be null if the binder doesn't support extended - * properties). - */ - void configure(String channelName, MessageChannel channel, - ProducerProperties producerProperties, T extendedProducerProperties); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/OutputBindingLifecycle.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/OutputBindingLifecycle.java deleted file mode 100644 index 747ad660d..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/OutputBindingLifecycle.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; - -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.util.CollectionUtils; - -/** - * Coordinates binding/unbinding of output binding targets in accordance to the lifecycle - * of the host context. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - */ -public class OutputBindingLifecycle extends AbstractBindingLifecycle { - - @SuppressWarnings("unused") - // It is actually used reflectively since at the moment we do not want to expose it - // via public method - private Collection> outputBindings = new ArrayList>(); - - public OutputBindingLifecycle(BindingService bindingService, - Map bindables) { - super(bindingService, bindables); - } - - /** - * Return a low value so that this bean is started after receiving Lifecycle beans are - * started. Beans that need to start before bindings will set a lower phase value. - */ - @Override - public int getPhase() { - return Integer.MIN_VALUE + 1000; - } - - @Override - void doStartWithBindable(Bindable bindable) { - Collection> bindableBindings = bindable - .createAndBindOutputs(this.bindingService); - if (!CollectionUtils.isEmpty(bindableBindings)) { - this.outputBindings.addAll(bindableBindings); - } - } - - @Override - void doStopWithBindable(Bindable bindable) { - bindable.unbindOutputs(this.bindingService); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/StreamAnnotationErrorMessages.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/StreamAnnotationErrorMessages.java deleted file mode 100644 index d648293b5..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/StreamAnnotationErrorMessages.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -/** - * @author Soby Chacko - */ -public abstract class StreamAnnotationErrorMessages { - - /** - * Annotation error message when no output was passed. - */ - public static final String ATLEAST_ONE_OUTPUT = "At least one output must be specified"; - - /** - * Annotation error message when multiple destinations were set. - */ - public static final String SEND_TO_MULTIPLE_DESTINATIONS = "Multiple destinations cannot be specified"; - - /** - * Annotation error message when an empty destination was specified. - */ - public static final String SEND_TO_EMPTY_DESTINATION = "An empty destination cannot be specified"; - - /** - * Annotation error message when an invalid outbound name was set. - */ - public static final String INVALID_OUTBOUND_NAME = "The @Output annotation must have the name of an input as value"; - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/SubscribableChannelBindingTargetFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/SubscribableChannelBindingTargetFactory.java deleted file mode 100644 index 5452a30af..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/SubscribableChannelBindingTargetFactory.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.messaging.SubscribableChannel; - -/** - * An implementation of {@link BindingTargetFactory} for creating - * {@link SubscribableChannel}s. - * - * @author Marius Bogoevici - * @author David Syer - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - * @author Byungjun You - */ -public class SubscribableChannelBindingTargetFactory - extends AbstractBindingTargetFactory { - - private final MessageChannelConfigurer messageChannelConfigurer; - - @Autowired - private GenericApplicationContext context; - - public SubscribableChannelBindingTargetFactory( - MessageChannelConfigurer messageChannelConfigurer) { - super(SubscribableChannel.class); - this.messageChannelConfigurer = messageChannelConfigurer; - } - - @Override - public SubscribableChannel createInput(String name) { - SubscribableChannel subscribableChannel = null; - if (context != null && context.containsBean(name)) { - try { - subscribableChannel = context.getBean(name, SubscribableChannel.class); - } - catch (BeanCreationException e) { - // ignore - /* - * Since we still support annotation-based programming model, this exception happens - * because of proxies related to @Input @Output - */ - } - } - if (subscribableChannel == null) { - DirectWithAttributesChannel channel = new DirectWithAttributesChannel(); - channel.setComponentName(name); - if (context != null && !context.containsBean(name)) { - context.registerBean(name, DirectWithAttributesChannel.class, () -> channel); - } - subscribableChannel = channel; - } - if (subscribableChannel instanceof DirectWithAttributesChannel directWithAttributesChannel) { - directWithAttributesChannel.setAttribute("type", "input"); - this.messageChannelConfigurer.configureInputChannel(directWithAttributesChannel, name); - } - - return subscribableChannel; - } - - @Override - public SubscribableChannel createOutput(String name) { - SubscribableChannel subscribableChannel = null; - if (context != null && context.containsBean(name)) { - try { - subscribableChannel = context.getBean(name, SubscribableChannel.class); - } - catch (BeanCreationException e) { - // ignore - } - } - if (subscribableChannel == null) { - DirectWithAttributesChannel channel = new DirectWithAttributesChannel(); - channel.setComponentName(name); - if (context != null && !context.containsBean(name)) { - context.registerBean(name, DirectWithAttributesChannel.class, () -> channel); - } - subscribableChannel = channel; - } - if (subscribableChannel instanceof DirectWithAttributesChannel directWithAttributesChannel) { - directWithAttributesChannel.setAttribute("type", "output"); - this.messageChannelConfigurer.configureOutputChannel(directWithAttributesChannel, name); - } - - return subscribableChannel; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/SupportedBindableFeatures.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/SupportedBindableFeatures.java deleted file mode 100644 index b182c9ff3..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/SupportedBindableFeatures.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.binding; - -/** - * Internal abstraction for the supported bindable features. - * - * @author Soby Chacko - * @since 4.0.0 - */ -public class SupportedBindableFeatures { - - private boolean pollable; - - private boolean reactive; - - public void setPollable(boolean pollable) { - this.pollable = pollable; - } - - public void setReactive(boolean reactive) { - this.reactive = reactive; - } - - public boolean isPollable() { - return pollable; - } - - public boolean isReactive() { - return reactive; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BinderFactoryAutoConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BinderFactoryAutoConfiguration.java deleted file mode 100644 index a30eb2ca2..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BinderFactoryAutoConfiguration.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.config; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.stream.binder.BinderType; -import org.springframework.cloud.stream.binder.BinderTypeRegistry; -import org.springframework.cloud.stream.binder.DefaultBinderTypeRegistry; -import org.springframework.cloud.stream.binding.CompositeMessageChannelConfigurer; -import org.springframework.cloud.stream.binding.FluxMessageChannelBindingTargetFactory; -import org.springframework.cloud.stream.binding.MessageChannelConfigurer; -import org.springframework.cloud.stream.binding.MessageConverterConfigurer; -import org.springframework.cloud.stream.binding.MessageSourceBindingTargetFactory; -import org.springframework.cloud.stream.binding.SubscribableChannelBindingTargetFactory; -import org.springframework.cloud.stream.function.StreamFunctionConfigurationProperties; -import org.springframework.cloud.stream.function.StreamFunctionProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Role; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.handler.support.MapArgumentResolver; -import org.springframework.integration.handler.support.PayloadExpressionArgumentResolver; -import org.springframework.integration.handler.support.PayloadsArgumentResolver; -import org.springframework.integration.support.NullAwarePayloadArgumentResolver; -import org.springframework.lang.Nullable; -import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; -import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; -import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; -import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; -import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.validation.Validator; - -/** - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - * @author Soby Chacko - * @author David Harrigan - */ -@Configuration(proxyBeanMethods = false) -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -@EnableConfigurationProperties({ BindingServiceProperties.class }) -@Import(ContentTypeConfiguration.class) -public class BinderFactoryAutoConfiguration { - - private static final String SPRING_CLOUD_STREAM_INTERNAL_PREFIX = "spring.cloud.stream.internal"; - - private static final String SELF_CONTAINED_APP_PROPERTY_NAME = SPRING_CLOUD_STREAM_INTERNAL_PREFIX - + ".selfContained"; - - protected final Log logger = LogFactory.getLog(getClass()); - - @Value("${" + SELF_CONTAINED_APP_PROPERTY_NAME + ":}") - private String selfContained; - - static Collection parseBinderConfigurations(ClassLoader classLoader, - Resource resource) throws IOException, ClassNotFoundException { - Properties properties = PropertiesLoaderUtils.loadProperties(resource); - Collection parsedBinderConfigurations = new ArrayList<>(); - for (Map.Entry entry : properties.entrySet()) { - String binderType = (String) entry.getKey(); - String[] binderConfigurationClassNames = StringUtils - .commaDelimitedListToStringArray((String) entry.getValue()); - Class[] binderConfigurationClasses = new Class[binderConfigurationClassNames.length]; - int i = 0; - for (String binderConfigurationClassName : binderConfigurationClassNames) { - binderConfigurationClasses[i++] = ClassUtils - .forName(binderConfigurationClassName, classLoader); - } - parsedBinderConfigurations - .add(new BinderType(binderType, binderConfigurationClasses)); - } - return parsedBinderConfigurations; - } - - - - @Bean(IntegrationContextUtils.MESSAGE_HANDLER_FACTORY_BEAN_NAME) - public static MessageHandlerMethodFactory messageHandlerMethodFactory( - @Qualifier(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME) CompositeMessageConverter compositeMessageConverter, - @Nullable Validator validator, ConfigurableListableBeanFactory clbf) { - - DefaultMessageHandlerMethodFactory messageHandlerMethodFactory = new DefaultMessageHandlerMethodFactory(); - messageHandlerMethodFactory.setMessageConverter(compositeMessageConverter); - - /* - * We essentially do the same thing as the - * DefaultMessageHandlerMethodFactory.initArgumentResolvers(..). We can't do it as - * custom resolvers for two reasons. 1. We would have two duplicate (compatible) - * resolvers, so they would need to be ordered properly to ensure these new - * resolvers take precedence. 2. - * DefaultMessageHandlerMethodFactory.initArgumentResolvers(..) puts - * MessageMethodArgumentResolver before custom converters thus not allowing an - * override which kind of proves #1. - * - * In all, all this will be obsolete once https://jira.spring.io/browse/SPR-17503 - * is addressed and we can fall back on core resolvers - */ - List resolvers = new LinkedList<>(); - resolvers.add(new SmartPayloadArgumentResolver( - compositeMessageConverter, - validator)); - resolvers.add(new SmartMessageMethodArgumentResolver( - compositeMessageConverter)); - - resolvers.add(new HeaderMethodArgumentResolver(clbf.getConversionService(), clbf)); - resolvers.add(new HeadersMethodArgumentResolver()); - - // Copy the order from Spring Integration for compatibility with SI 5.2 - resolvers.add(new PayloadExpressionArgumentResolver()); - resolvers.add(new NullAwarePayloadArgumentResolver(compositeMessageConverter)); - PayloadExpressionArgumentResolver payloadExpressionArgumentResolver = new PayloadExpressionArgumentResolver(); - payloadExpressionArgumentResolver.setBeanFactory(clbf); - resolvers.add(payloadExpressionArgumentResolver); - PayloadsArgumentResolver payloadsArgumentResolver = new PayloadsArgumentResolver(); - payloadsArgumentResolver.setBeanFactory(clbf); - resolvers.add(payloadsArgumentResolver); - MapArgumentResolver mapArgumentResolver = new MapArgumentResolver(); - mapArgumentResolver.setBeanFactory(clbf); - resolvers.add(mapArgumentResolver); - - messageHandlerMethodFactory.setArgumentResolvers(resolvers); - messageHandlerMethodFactory.setValidator(validator); - return messageHandlerMethodFactory; - } - - @Bean - public BinderTypeRegistry binderTypeRegistry( - ConfigurableApplicationContext configurableApplicationContext) { - Map binderTypes = new HashMap<>(); - ClassLoader classLoader = configurableApplicationContext.getClassLoader(); - try { - Enumeration resources = classLoader.getResources("META-INF/spring.binders"); - - if (binderTypes.isEmpty() && !Boolean.valueOf(this.selfContained) - && (resources == null || !resources.hasMoreElements())) { - this.logger.debug( - "Failed to locate 'META-INF/spring.binders' resources on the classpath." - + " Assuming standard boot 'META-INF/spring.factories' configuration is used"); - } - else { - while (resources.hasMoreElements()) { - URL url = resources.nextElement(); - UrlResource resource = new UrlResource(url); - for (BinderType binderType : parseBinderConfigurations(classLoader, resource)) { - binderTypes.put(binderType.getDefaultName(), binderType); - } - } - } - } - catch (IOException | ClassNotFoundException e) { - throw new BeanCreationException("Cannot create binder factory:", e); - } - return new DefaultBinderTypeRegistry(binderTypes); - } - - @Bean - public StreamFunctionProperties streamFunctionProperties(@Nullable StreamFunctionConfigurationProperties properties) { - StreamFunctionProperties streamFunctionProperties = new StreamFunctionProperties(); - if (properties != null) { - streamFunctionProperties.setBindings(properties.getBindings()); - } - return streamFunctionProperties; - } - - @Bean - public MessageConverterConfigurer messageConverterConfigurer( - BindingServiceProperties bindingServiceProperties, - @Qualifier(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME) CompositeMessageConverter compositeMessageConverter, - @Nullable StreamFunctionProperties streamFunctionProperties) { - - return new MessageConverterConfigurer(bindingServiceProperties, compositeMessageConverter, streamFunctionProperties); - } - - - @Bean - public SubscribableChannelBindingTargetFactory channelFactory( - CompositeMessageChannelConfigurer compositeMessageChannelConfigurer) { - return new SubscribableChannelBindingTargetFactory( - compositeMessageChannelConfigurer); - } - - @Bean - public FluxMessageChannelBindingTargetFactory fluxMessageChannelBindingTargetFactory( - CompositeMessageChannelConfigurer compositeMessageChannelConfigurer, GenericApplicationContext context) { - return new FluxMessageChannelBindingTargetFactory( - compositeMessageChannelConfigurer, context); - } - - @Bean - public MessageSourceBindingTargetFactory messageSourceFactory( - @Qualifier(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME) CompositeMessageConverter compositeMessageConverter, - CompositeMessageChannelConfigurer compositeMessageChannelConfigurer) { - return new MessageSourceBindingTargetFactory(compositeMessageConverter, - compositeMessageChannelConfigurer); - } - - @Bean - public CompositeMessageChannelConfigurer compositeMessageChannelConfigurer( - MessageConverterConfigurer messageConverterConfigurer) { - List configurerList = new ArrayList<>(); - configurerList.add(messageConverterConfigurer); - return new CompositeMessageChannelConfigurer(configurerList); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BinderProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BinderProperties.java deleted file mode 100644 index 0e1788205..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BinderProperties.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import java.util.HashMap; -import java.util.Map; - -/** - * Contains the properties of a binder. - * - * @author Marius Bogoevici - * @author Oleg Zhurakousky - */ -public class BinderProperties { - - /** - * The binder type. It typically references one of the binders found on the classpath, - * in particular a key in a META-INF/spring.binders file. By default, it has the same - * value as the configuration name. - */ - private String type; - - /** - * Root for a set of properties that can be used to customize the environment of the - * binder. - */ - private Map environment = new HashMap<>(); - - /** - * Whether the configuration will inherit the environment of the application itself. - * Default: true - */ - private boolean inheritEnvironment = true; - - /** - * Whether the binder configuration is a candidate for being considered a default - * binder, or can be used only when explicitly referenced. Default: true - */ - private boolean defaultCandidate = true; - - public String getType() { - return this.type; - } - - public void setType(String name) { - this.type = name; - } - - public Map getEnvironment() { - return this.environment; - } - - - public void setEnvironment(Map environment) { - this.environment = environment; - } - - public boolean isInheritEnvironment() { - return this.inheritEnvironment; - } - - public void setInheritEnvironment(boolean inheritEnvironment) { - this.inheritEnvironment = inheritEnvironment; - } - - public boolean isDefaultCandidate() { - return this.defaultCandidate; - } - - public void setDefaultCandidate(boolean defaultCandidate) { - this.defaultCandidate = defaultCandidate; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindersHealthIndicatorAutoConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindersHealthIndicatorAutoConfiguration.java deleted file mode 100644 index 8756a41d8..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindersHealthIndicatorAutoConfiguration.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.locks.ReentrantLock; - -import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.CompositeHealthContributor; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.NamedContributor; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.DefaultBinderFactory; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; - -/** - * @author Ilayaperumal Gopinathan - * @author Soby Chacko - */ -@ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") -@ConditionalOnEnabledHealthIndicator("binders") -@AutoConfigureBefore(EndpointAutoConfiguration.class) -@ConditionalOnBean(BinderFactory.class) -@AutoConfigureAfter(BindingServiceConfiguration.class) -@AutoConfiguration -public class BindersHealthIndicatorAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public BindersHealthContributor bindersHealthContributor() { - return new BindersHealthContributor(); - } - - @Bean - public DefaultBinderFactory.Listener bindersHealthIndicatorListener( - BindersHealthContributor bindersHealthContributor) { - return new BindersHealthIndicatorListener(bindersHealthContributor); - } - - /** - * A {@link DefaultBinderFactory.Listener} that provides {@link HealthIndicator} - * support. - */ - public static class BindersHealthIndicatorListener - implements DefaultBinderFactory.Listener { - - private final BindersHealthContributor bindersHealthContributor; - - BindersHealthIndicatorListener(BindersHealthContributor bindersHealthContributor) { - this.bindersHealthContributor = bindersHealthContributor; - } - - @Override - public void afterBinderContextInitialized(String binderConfigurationName, - ConfigurableApplicationContext binderContext) { - if (this.bindersHealthContributor != null) { - this.bindersHealthContributor.add(binderConfigurationName, - binderContext.getBeansOfType(HealthContributor.class)); - } - } - - } - - /** - * {@link CompositeHealthContributor} that provides binder health contributions. - */ - public static class BindersHealthContributor implements CompositeHealthContributor { - - private static final HealthIndicator UNKNOWN = () -> Health.unknown().build(); - - private volatile Map contributors = Collections.emptyMap(); - - private final ReentrantLock lock = new ReentrantLock(); - - void add(String binderConfigurationName, Map binderHealthContributors) { - // if there are no health contributors in the child context, we just mark - // the binder's health as unknown - // this can happen due to the fact that configuration is inherited - this.lock.lock(); - try { - Map newContributors = new LinkedHashMap<>(this.contributors); - HealthContributor contributor = obtainContributor(binderHealthContributors); - newContributors.put(binderConfigurationName, contributor); - this.contributors = newContributors; - } - finally { - this.lock.unlock(); - } - } - - private HealthContributor obtainContributor(Map binderHealthContributors) { - if (binderHealthContributors.isEmpty()) { - return UNKNOWN; - } - if (binderHealthContributors.size() == 1) { - return binderHealthContributors.values().iterator().next(); - } - return CompositeHealthContributor.fromMap(binderHealthContributors); - } - - @Override - public HealthContributor getContributor(String name) { - return contributors.get(name); - } - - @Override - public Iterator> iterator() { - return contributors.entrySet().stream() - .map((entry) -> NamedContributor.of(entry.getKey(), entry.getValue())).iterator(); - } - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingHandlerAdvise.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingHandlerAdvise.java deleted file mode 100644 index 63737a49f..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingHandlerAdvise.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.config; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor; -import org.springframework.boot.context.properties.bind.AbstractBindHandler; -import org.springframework.boot.context.properties.bind.BindContext; -import org.springframework.boot.context.properties.bind.BindHandler; -import org.springframework.boot.context.properties.bind.BindResult; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; -import org.springframework.util.CollectionUtils; - -/** - * @author Oleg Zhurakousky - * @since 2.1 - * - */ -public class BindingHandlerAdvise implements ConfigurationPropertiesBindHandlerAdvisor { - - private final Map mappings; - - - BindingHandlerAdvise( - Map additionalMappings) { - this.mappings = new LinkedHashMap<>(); - this.mappings.put(ConfigurationPropertyName.of("spring.cloud.stream.bindings"), - ConfigurationPropertyName.of("spring.cloud.stream.default")); - if (!CollectionUtils.isEmpty(additionalMappings)) { - this.mappings.putAll(additionalMappings); - } - } - - @Override - public BindHandler apply(BindHandler bindHandler) { - - - BindHandler handler = new AbstractBindHandler(bindHandler) { - @Override - public Bindable onStart(ConfigurationPropertyName name, - Bindable target, BindContext context) { - ConfigurationPropertyName defaultName = getDefaultName(name); - if (defaultName != null) { - BindResult result = context.getBinder().bind(defaultName, target); - if (result.isBound()) { - return target.withExistingValue(result.get()); - } - } - return bindHandler.onStart(name, target, context); - } - }; - return handler; - } - - private ConfigurationPropertyName getDefaultName(ConfigurationPropertyName name) { - for (Map.Entry mapping : this.mappings - .entrySet()) { - ConfigurationPropertyName from = mapping.getKey(); - ConfigurationPropertyName to = mapping.getValue(); - if ((from.isAncestorOf(name) - && name.getNumberOfElements() > from.getNumberOfElements())) { - ConfigurationPropertyName defaultName = to; - for (int i = from.getNumberOfElements() + 1; i < name - .getNumberOfElements(); i++) { - defaultName = defaultName.append(name.getElement(i, Form.UNIFORM)); - } - return defaultName; - } - } - return null; - } - - /** - * Provides mappings including the default mappings. - */ - public interface MappingsProvider { - - Map getDefaultMappings(); - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingProperties.java deleted file mode 100644 index 7b0e2765c..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingProperties.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; - -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; -import org.springframework.validation.annotation.Validated; - -/** - * Contains the properties of a binding. - * - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Soby Chacko - * @author Oleg Zhurakousky - * @author Omer Celik - */ -@JsonInclude(Include.NON_DEFAULT) -@Validated -public class BindingProperties { - - /** - * Default content type for bindings. - */ - public static final MimeType DEFAULT_CONTENT_TYPE = MimeTypeUtils.APPLICATION_JSON; - - private static final String COMMA = ","; - - /** - * Function that corresponds to the error handler of the underlying function. - */ - private String errorHandlerDefinition; - - /** - * The physical name at the broker that the binder binds to. - */ - private String destination; - - /** - * Unique name that the binding belongs to (applies to consumers only). Multiple - * consumers within the same group share the subscription. A null or empty String - * value indicates an anonymous group that is not shared. - * @see org.springframework.cloud.stream.binder.Binder#bindConsumer(java.lang.String, - * java.lang.String, java.lang.Object, - * org.springframework.cloud.stream.binder.ConsumerProperties) - */ - private String group; - - // Properties for both input and output bindings - - /** - * Specifies content-type that will be used by this binding in the event it is not - * specified in Message headers. Default: 'application/json'. - */ - private String contentType = DEFAULT_CONTENT_TYPE.toString(); - - /** - * The name of the binder to use for this binding in the event multiple binders - * available (e.g., 'rabbit'). - */ - private String binder; - - /** - * Additional consumer specific properties (see {@link ConsumerProperties}). - */ - @NestedConfigurationProperty - private ConsumerProperties consumer; - - /** - * Additional producer specific properties (see {@link ProducerProperties}). - */ - @NestedConfigurationProperty - private ProducerProperties producer; - - public String getDestination() { - return this.destination; - } - - public void setDestination(String destination) { - this.destination = destination; - } - - public String getGroup() { - return this.group; - } - - public void setGroup(String group) { - this.group = group; - } - - public String getContentType() { - return this.contentType; - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public String getBinder() { - return this.binder; - } - - public void setBinder(String binder) { - this.binder = binder; - } - - public ConsumerProperties getConsumer() { - return this.consumer; - } - - public void setConsumer(ConsumerProperties consumer) { - this.consumer = consumer; - } - - public ProducerProperties getProducer() { - return this.producer; - } - - public void setProducer(ProducerProperties producer) { - this.producer = producer; - } - - public boolean onlyOneOfProducerOrConsumerSet() { - return this.consumer == null || this.producer == null; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("destination=" + this.destination); - sb.append(COMMA); - sb.append("group=" + this.group); - sb.append(COMMA); - if (this.contentType != null) { - sb.append("contentType=" + this.contentType); - sb.append(COMMA); - } - if (this.binder != null) { - sb.append("binder=" + this.binder); - sb.append(COMMA); - } - sb.deleteCharAt(sb.lastIndexOf(COMMA)); - return "BindingProperties{" + sb.toString() + "}"; - } - - public String getErrorHandlerDefinition() { - return errorHandlerDefinition; - } - - public void setErrorHandlerDefinition(String errorHandlerDefinition) { - this.errorHandlerDefinition = errorHandlerDefinition; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java deleted file mode 100644 index 56a6f9f0c..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceConfiguration.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.config; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.SearchStrategy; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.cloud.stream.binder.BinderChildContextInitializer; -import org.springframework.cloud.stream.binder.BinderConfiguration; -import org.springframework.cloud.stream.binder.BinderCustomizer; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.BinderType; -import org.springframework.cloud.stream.binder.BinderTypeRegistry; -import org.springframework.cloud.stream.binder.DefaultBinderFactory; -import org.springframework.cloud.stream.binding.Bindable; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.binding.BindingsLifecycleController; -import org.springframework.cloud.stream.binding.ContextStartAfterRefreshListener; -import org.springframework.cloud.stream.binding.DynamicDestinationsBindable; -import org.springframework.cloud.stream.binding.InputBindingLifecycle; -import org.springframework.cloud.stream.binding.OutputBindingLifecycle; -import org.springframework.cloud.stream.config.BindingHandlerAdvise.MappingsProvider; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Role; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.integration.channel.PublishSubscribeChannel; -import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; -import org.springframework.lang.Nullable; -import org.springframework.messaging.MessageChannel; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - -/** - * Configuration class that provides necessary beans for {@link MessageChannel} binding. - * - * @author Dave Syer - * @author David Turanski - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Gary Russell - * @author Vinicius Carvalho - * @author Artem Bilan - * @author Oleg Zhurakousky - * @author Soby Chacko - * @author Chris Bono - */ -@AutoConfiguration -@EnableConfigurationProperties({ BindingServiceProperties.class, - SpringIntegrationProperties.class}) -@Import({ SpelExpressionConverterConfiguration.class }) -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -@ConditionalOnBean(value = BinderTypeRegistry.class, search = SearchStrategy.CURRENT) -public class BindingServiceConfiguration { - - @Autowired(required = false) - private Collection binderFactoryListeners; - - public static Map getBinderConfigurations( - BinderTypeRegistry binderTypeRegistry, - BindingServiceProperties bindingServiceProperties) { - - Map binderConfigurations = new HashMap<>(); - Map declaredBinders = bindingServiceProperties - .getBinders(); - boolean defaultCandidatesExist = false; - Iterator> binderPropertiesIterator = declaredBinders - .entrySet().iterator(); - while (!defaultCandidatesExist && binderPropertiesIterator.hasNext()) { - defaultCandidatesExist = binderPropertiesIterator.next().getValue() - .isDefaultCandidate(); - } - List existingBinderConfigurations = new ArrayList<>(); - for (Map.Entry binderEntry : declaredBinders - .entrySet()) { - BinderProperties binderProperties = binderEntry.getValue(); - if (binderTypeRegistry.get(binderEntry.getKey()) != null) { - binderConfigurations.put(binderEntry.getKey(), - new BinderConfiguration(binderEntry.getKey(), - binderProperties.getEnvironment(), - binderProperties.isInheritEnvironment(), - binderProperties.isDefaultCandidate())); - existingBinderConfigurations.add(binderEntry.getKey()); - } - else { - Assert.hasText(binderProperties.getType(), - "No 'type' property present for custom binder " - + binderEntry.getKey()); - binderConfigurations.put(binderEntry.getKey(), - new BinderConfiguration(binderProperties.getType(), - binderProperties.getEnvironment(), - binderProperties.isInheritEnvironment(), - binderProperties.isDefaultCandidate())); - existingBinderConfigurations.add(binderEntry.getKey()); - } - } - for (Map.Entry configurationEntry : binderConfigurations - .entrySet()) { - if (configurationEntry.getValue().isDefaultCandidate()) { - defaultCandidatesExist = true; - } - } - if (!defaultCandidatesExist) { - for (Map.Entry binderEntry : binderTypeRegistry.getAll() - .entrySet()) { - if (!existingBinderConfigurations.contains(binderEntry.getKey())) { - binderConfigurations.put(binderEntry.getKey(), - new BinderConfiguration(binderEntry.getKey(), new HashMap<>(), - true, true)); // true, !"integration".equals(binderEntry.getKey()))); - } - } - } - return binderConfigurations; - } - - @Bean - public static BeanPostProcessor globalErrorChannelCustomizer() { - return new BeanPostProcessor() { - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if ("errorChannel".equals(beanName)) { - ((PublishSubscribeChannel) bean).setIgnoreFailures(true); - } - return bean; - } - }; - } - - @Bean - public BindingHandlerAdvise BindingHandlerAdvise( - @Nullable MappingsProvider[] providers) { - Map additionalMappings = new HashMap<>(); - if (!ObjectUtils.isEmpty(providers)) { - for (int i = 0; i < providers.length; i++) { - MappingsProvider mappingsProvider = providers[i]; - additionalMappings.putAll(mappingsProvider.getDefaultMappings()); - } - } - return new BindingHandlerAdvise(additionalMappings); - } - - @Bean - @ConditionalOnMissingBean(BinderFactory.class) - public DefaultBinderFactory binderFactory(BinderTypeRegistry binderTypeRegistry, - BindingServiceProperties bindingServiceProperties, - ObjectProvider binderCustomizerProvider, - BinderChildContextInitializer binderChildContextInitializer) { - DefaultBinderFactory binderFactory = new DefaultBinderFactory( - getBinderConfigurations(binderTypeRegistry, bindingServiceProperties), - binderTypeRegistry, binderCustomizerProvider.getIfUnique()); - binderFactory.setDefaultBinder(bindingServiceProperties.getDefaultBinder()); - binderFactory.setListeners(this.binderFactoryListeners); - binderChildContextInitializer.setBinderFactory(binderFactory); - return binderFactory; - } - - @Bean - public BinderChildContextInitializer binderChildContextInitializer() { - return new BinderChildContextInitializer(); - } - - @Bean - // This conditional is intentionally not in an autoconfig (usually a bad idea) because - // it is used to detect a BindingService in the parent context (which we know - // already exists). - @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) - public BindingService bindingService( - BindingServiceProperties bindingServiceProperties, - BinderFactory binderFactory, TaskScheduler taskScheduler, @Nullable ObjectMapper objectMapper) { - objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper; - return new BindingService(bindingServiceProperties, binderFactory, taskScheduler, objectMapper); - } - - @Bean - @DependsOn("bindingService") - public OutputBindingLifecycle outputBindingLifecycle(BindingService bindingService, - Map bindables) { - - return new OutputBindingLifecycle(bindingService, bindables); - } - - @Bean - @DependsOn("bindingService") - public InputBindingLifecycle inputBindingLifecycle(BindingService bindingService, - Map bindables) { - return new InputBindingLifecycle(bindingService, bindables); - } - - @Bean - public BindingsLifecycleController bindingsLifecycleController(List inputBindingLifecycles, - List outputBindingsLifecycles) { - return new BindingsLifecycleController(inputBindingLifecycles, outputBindingsLifecycles); - } - - @Bean - @DependsOn("bindingService") - public ContextStartAfterRefreshListener contextStartAfterRefreshListener() { - return new ContextStartAfterRefreshListener(); - } - - @Bean - public DynamicDestinationsBindable dynamicDestinationsBindable() { - return new DynamicDestinationsBindable(); - } - - @Bean - public ApplicationListener appListener( - SpringIntegrationProperties springIntegrationProperties) { - return new ApplicationListener() { - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - event.getApplicationContext() - .getBeansOfType(AbstractReplyProducingMessageHandler.class) - .values() - .forEach(mh -> mh - .addNotPropagatedHeaders(springIntegrationProperties - .getMessageHandlerNotPropagatedHeaders())); - } - - }; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceProperties.java deleted file mode 100644 index a7b6575d1..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingServiceProperties.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver; -import org.springframework.boot.context.properties.source.ConfigurationPropertySources; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.integration.support.utils.IntegrationUtils; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * @author Dave Syer - * @author Marius Bogoevici - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - * @author Michael Michailidis - */ -@ConfigurationProperties("spring.cloud.stream") -@JsonInclude(Include.NON_DEFAULT) -public class BindingServiceProperties - implements ApplicationContextAware, InitializingBean { - - private static final int DEFAULT_BINDING_RETRY_INTERVAL = 30; - - /** - * A semi-colon delimited string representing the names of the sources based on which source bindings will be created. - * This is primarily to support cases where source binding may be required without providing a corresponding Supplier. - * (e.g., for cases where the actual source of data is outside of scope of spring-cloud-stream - HTTP -> Stream) - * @deprecated use {@link #outputBindings} - */ - @Deprecated - private String source; - - /** - * A semi-colon delimited string to explicitly define input bindings (specifically for cases when there - * is no implicit trigger to create such bindings such as Function, Supplier or Consumer). - */ - private String inputBindings; - - /** - * A semi-colon delimited string to explicitly define output bindings (specifically for cases when there - * is no implicit trigger to create such bindings such as Function, Supplier or Consumer). - */ - private String outputBindings; - - /** - * The instance id of the application: a number from 0 to instanceCount-1. Used for - * partitioning and with Kafka. NOTE: Could also be managed per individual binding - * "spring.cloud.stream.bindings.foo.consumer.instance-index" where 'foo' is the name - * of the binding. - */ - @Value("${INSTANCE_INDEX:${CF_INSTANCE_INDEX:0}}") - private int instanceIndex; - - /** - * A list of instance id's from 0 to instanceCount-1. Used for partitioning and with - * Kafka. NOTE: Could also be managed per individual binding - * "spring.cloud.stream.bindings.foo.consumer.instance-index-list" where 'foo' is - * the name of the binding. This setting will override the one set in - * 'spring.cloud.stream.instance-index' - */ - private List instanceIndexList = new ArrayList<>(); - - /** - * The number of deployed instances of an application. Default: 1. NOTE: Could also be - * managed per individual binding - * "spring.cloud.stream.bindings.foo.consumer.instance-count" where 'foo' is the name - * of the binding. - */ - private int instanceCount = 1; - - /** - * Additional binding properties (see {@link BinderProperties}) per binding name - * (e.g., 'input`). - * - * For example; This sets the content-type for the 'input' binding of a Sink - * application: 'spring.cloud.stream.bindings.input.contentType=text/plain' - */ - private Map bindings = new TreeMap<>( - String.CASE_INSENSITIVE_ORDER); - - /** - * Additional per-binder properties (see {@link BinderProperties}) if more then one - * binder of the same type is used (i.e., connect to multiple instances of RabbitMq). - * Here you can specify multiple binder configurations, each with different - * environment settings. For example; spring.cloud.stream.binders.rabbit1.environment. - * . . , spring.cloud.stream.binders.rabbit2.environment. . . - */ - private Map binders = new HashMap<>(); - - /** - * The name of the binder to use by all bindings in the event multiple binders - * available (e.g., 'rabbit'). - */ - private String defaultBinder; - - /** - * A list of destinations that can be bound dynamically. If set, only listed - * destinations can be bound. - */ - private String[] dynamicDestinations = new String[0]; - - /** - * The maximum size of Least Recently Used (LRU) cache of dynamic destinations. Once - * this size is reached, new destinations will trigger the removal of old destinations. - * Default: 10 - */ - private int dynamicDestinationCacheSize = 10; - - /** - * Retry interval (in seconds) used to schedule binding attempts. Default: 30 sec. - */ - private int bindingRetryInterval = DEFAULT_BINDING_RETRY_INTERVAL; - - private ConfigurableApplicationContext applicationContext = new GenericApplicationContext(); - - private ConversionService conversionService; - - public Map getBindings() { - return this.bindings; - } - - public void setBindings(Map bindings) { - this.bindings.putAll(bindings); - } - - public Map getBinders() { - return this.binders; - } - - public void setBinders(Map binders) { - this.binders = binders; - } - - public String getDefaultBinder() { - return this.defaultBinder; - } - - public void setDefaultBinder(String defaultBinder) { - this.defaultBinder = defaultBinder; - } - - public int getInstanceIndex() { - return this.instanceIndex; - } - - public void setInstanceIndex(int instanceIndex) { - this.instanceIndex = instanceIndex; - } - - public List getInstanceIndexList() { - return this.instanceIndexList; - } - - public void setInstanceIndexList(List instanceIndexList) { - this.instanceIndexList = instanceIndexList; - } - - public int getInstanceCount() { - return this.instanceCount; - } - - public void setInstanceCount(int instanceCount) { - this.instanceCount = instanceCount; - } - - public String[] getDynamicDestinations() { - return this.dynamicDestinations; - } - - public void setDynamicDestinations(String[] dynamicDestinations) { - this.dynamicDestinations = dynamicDestinations; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) - throws BeansException { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; - GenericConversionService cs = (GenericConversionService) IntegrationUtils - .getConversionService(this.applicationContext.getBeanFactory()); - if (this.applicationContext.containsBean("spelConverter")) { - Converter converter = (Converter) this.applicationContext - .getBean("spelConverter"); - cs.addConverter(converter); - } - } - - public void setConversionService(ConversionService conversionService) { - this.conversionService = conversionService; - } - - @Override - public void afterPropertiesSet() throws Exception { - if (this.conversionService == null) { - this.conversionService = this.applicationContext.getBean( - IntegrationUtils.INTEGRATION_CONVERSION_SERVICE_BEAN_NAME, - ConversionService.class); - } - } - - public String getBinderType(String binderName) { - BinderProperties bp = this.binders.get(binderName); - return bp != null ? bp.getType() : this.bindings.keySet().iterator().next(); - } - - public String getBinder(String bindingName) { - return getBindingProperties(bindingName).getBinder(); - } - - /** - * Return configuration properties as Map. - * @return map of binding configuration properties. - */ - public Map asMapProperties() { - Map properties = new HashMap<>(); - properties.put("instanceIndex", String.valueOf(getInstanceIndex())); - properties.put("instanceCount", String.valueOf(getInstanceCount())); - properties.put("defaultBinder", getDefaultBinder()); - properties.put("dynamicDestinations", getDynamicDestinations()); - for (Map.Entry entry : this.bindings.entrySet()) { - properties.put(entry.getKey(), entry.getValue().toString()); - } - for (Map.Entry entry : this.binders.entrySet()) { - properties.put(entry.getKey(), entry.getValue()); - } - return properties; - } - - public ConsumerProperties getConsumerProperties(String inputBindingName) { - Assert.notNull(inputBindingName, "The input binding name cannot be null"); - BindingProperties bindingProperties = getBindingProperties(inputBindingName); - ConsumerProperties consumerProperties = bindingProperties.getConsumer(); - if (consumerProperties == null) { - consumerProperties = new ConsumerProperties(); - bindingProperties.setConsumer(consumerProperties); - } - // propagate instance count and instance index if not already set - if (consumerProperties.getInstanceCount() < 0) { - consumerProperties.setInstanceCount(this.instanceCount); - } - if (consumerProperties.getInstanceIndex() < 0) { - consumerProperties.setInstanceIndex(this.instanceIndex); - } - if (consumerProperties.getInstanceIndexList() == null) { - consumerProperties.setInstanceIndexList(this.instanceIndexList); - } - return consumerProperties; - } - - public ProducerProperties getProducerProperties(String outputBindingName) { - Assert.notNull(outputBindingName, "The output binding name cannot be null"); - BindingProperties bindingProperties = getBindingProperties(outputBindingName); - ProducerProperties producerProperties = bindingProperties.getProducer(); - if (producerProperties == null) { - producerProperties = new ProducerProperties(); - bindingProperties.setProducer(producerProperties); - } - return producerProperties; - } - - public BindingProperties getBindingProperties(String bindingName) { - this.bindIfNecessary(bindingName); - BindingProperties bindingProperties = this.bindings.get(bindingName); - if (bindingProperties.getDestination() == null) { - bindingProperties.setDestination(bindingName); - } - return bindingProperties; - } - - public String getGroup(String bindingName) { - return getBindingProperties(bindingName).getGroup(); - } - - public String getBindingDestination(String bindingName) { - return getBindingProperties(bindingName).getDestination(); - } - - public int getBindingRetryInterval() { - return this.bindingRetryInterval; - } - - public void setBindingRetryInterval(int bindingRetryInterval) { - this.bindingRetryInterval = bindingRetryInterval; - } - - /** - * @deprecated in favor of {@link #getOutputBindings()} - */ - @Deprecated - public String getSource() { - return source; - } - - /** - * @deprecated in favor of {@link #setOutputBindings()} - */ - @Deprecated - public void setSource(String source) { - this.source = source; - this.outputBindings = source; - } - - public void updateProducerProperties(String bindingName, - ProducerProperties producerProperties) { - if (this.bindings.containsKey(bindingName)) { - this.bindings.get(bindingName).setProducer(producerProperties); - } - } - - public int getDynamicDestinationCacheSize() { - return dynamicDestinationCacheSize; - } - - public void setDynamicDestinationCacheSize(int dynamicDestinationCacheSize) { - this.dynamicDestinationCacheSize = dynamicDestinationCacheSize; - } - - public String getInputBindings() { - return inputBindings; - } - - public void setInputBindings(String inputBindings) { - this.inputBindings = inputBindings; - } - - public String getOutputBindings() { - - return outputBindings; - } - - public void setOutputBindings(String outputBindings) { - Assert.state(!StringUtils.hasText(this.source), "Setting 'source' and 'output-binding' is not allowed " - + "because 'source' is deprecated in favor of 'output-binding'."); - this.outputBindings = outputBindings; - } - - /* - * The "necessary" implies the scenario where only defaults are defined. - */ - private void bindIfNecessary(String bindingName) { - if (!this.bindings.containsKey(bindingName)) { - this.bindToDefault(bindingName); - } - } - - private void bindToDefault(String binding) { - BindingProperties bindingPropertiesTarget = new BindingProperties(); - Binder binder = new Binder( - ConfigurationPropertySources - .get(this.applicationContext.getEnvironment()), - new PropertySourcesPlaceholdersResolver( - this.applicationContext.getEnvironment()), - IntegrationUtils.getConversionService( - this.applicationContext.getBeanFactory()), - null); - binder.bind("spring.cloud.stream.default", - Bindable.ofInstance(bindingPropertiesTarget)); - this.bindings.put(binding, bindingPropertiesTarget); - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingsEndpointAutoConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingsEndpointAutoConfiguration.java deleted file mode 100644 index ea759db58..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingsEndpointAutoConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.binding.BindingsLifecycleController; -import org.springframework.cloud.stream.endpoint.BindingsEndpoint; -import org.springframework.context.annotation.Bean; - -/** - * @author Oleg Zhurakousky - * @since 2.0 - */ -@AutoConfiguration -@ConditionalOnClass(name = { - "org.springframework.boot.actuate.endpoint.annotation.Endpoint" }) -@ConditionalOnBean(BindingService.class) -@AutoConfigureAfter(EndpointAutoConfiguration.class) -public class BindingsEndpointAutoConfiguration { - - @Bean - @ConditionalOnAvailableEndpoint - public BindingsEndpoint bindingsEndpoint(BindingsLifecycleController bindingsLifecycleController) { - return new BindingsEndpoint(bindingsLifecycleController); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ChannelsEndpointAutoConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ChannelsEndpointAutoConfiguration.java deleted file mode 100644 index 88cde2152..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ChannelsEndpointAutoConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.endpoint.ChannelsEndpoint; -import org.springframework.context.annotation.Bean; - -/** - * @author Dave Syer - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - */ -@AutoConfiguration -@ConditionalOnClass(name = "org.springframework.boot.actuate.endpoint.annotation.Endpoint") -@ConditionalOnBean(BindingService.class) -@AutoConfigureAfter(EndpointAutoConfiguration.class) -public class ChannelsEndpointAutoConfiguration { - - @Bean - @ConditionalOnAvailableEndpoint - public ChannelsEndpoint channelsEndpoint(BindingServiceProperties properties) { - return new ChannelsEndpoint(properties); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ConsumerEndpointCustomizer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ConsumerEndpointCustomizer.java deleted file mode 100644 index 4eeaa6432..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ConsumerEndpointCustomizer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.config; - -import org.springframework.integration.core.MessageProducer; - -/** - * If a single bean of this type is in the application context, an inbound channel adapter - * created by the binder can be further customized after all the properties are set. For - * example, to configure less-common properties. - * - * @param {@link MessageProducer} type - * - * @author Gary Russell - * - * @since 3.0 - */ -@FunctionalInterface -public interface ConsumerEndpointCustomizer { - - /** - * Configure a {@link MessageProducer} that is being created by the binder for the - * provided destination name and group. - * @param endpoint the {@link MessageProducer} from the binder. - * @param destinationName the bound destination name. - * @param group the group. - */ - void configure(E endpoint, String destinationName, String group); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ContentTypeConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ContentTypeConfiguration.java deleted file mode 100644 index 551daf9a5..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ContentTypeConfiguration.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.config; - -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.cloud.function.json.JsonMapper; -import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.lang.Nullable; -import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.converter.MessageConverter; - -/** - * @author Vinicius Carvalho - * @author Artem Bilan - * @author Oleg Zhurakousky - */ -@Configuration(proxyBeanMethods = false) -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -class ContentTypeConfiguration { - - @Bean(name = IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME) - public CompositeMessageConverter configurableCompositeMessageConverter( - ObjectProvider objectMapperObjectProvider, - List customMessageConverters, @Nullable JsonMapper jsonMapper) { - - customMessageConverters = customMessageConverters.stream() - .filter(c -> isConverterEligible(c)).collect(Collectors.toList()); - - CompositeMessageConverterFactory factory = - new CompositeMessageConverterFactory(customMessageConverters, objectMapperObjectProvider.getIfAvailable(ObjectMapper::new), jsonMapper); - - return factory.getMessageConverterForAllRegistered(); - } - - /* - * We want to filter out all non-stream MessageConverters, given that other - * auto-configurations may interfere with their MessageConverters. - */ - private boolean isConverterEligible(Object messageConverter) { - String messageConverterName = messageConverter.getClass().getName(); - if (messageConverterName.startsWith("org.springframework.cloud.")) { - return true; - } - else if (!messageConverterName.startsWith("org.springframework.")) { - return true; - } - return false; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ListenerContainerCustomizer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ListenerContainerCustomizer.java deleted file mode 100644 index a63b3c176..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ListenerContainerCustomizer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.config; - -/** - * If a single bean of this type is in the application context, listener containers - * created by the binder can be further customized after all the properties are set. For - * example, to configure less-common properties. - * - * @param container type - * @author Gary Russell - * @author Oleg Zhurakousky - * @since 2.1 - */ -@FunctionalInterface -public interface ListenerContainerCustomizer { - - /** - * Configure the container that is being created for the supplied queue name and - * consumer group. - * @param container the container. - * @param destinationName the destination name. - * @param group the consumer group. - */ - void configure(T container, String destinationName, String group); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/MessageSourceCustomizer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/MessageSourceCustomizer.java deleted file mode 100644 index 97be0533c..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/MessageSourceCustomizer.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.config; - -import org.springframework.integration.core.MessageSource; - -/** - * If a single bean of this type is in the application context, pollable message sources - * created by the binder can be further customized after all the properties are set. For - * example, to configure less-common properties. - * - * @param {@link MessageSource} type - * @author Gary Russell - * @since 2.2 - */ -@FunctionalInterface -public interface MessageSourceCustomizer { - - /** - * Configure the container that is being created for the supplied queue name and - * consumer group. - * @param source the MessageSource. - * @param destinationName the destination name. - * @param group the consumer group. - */ - void configure(T source, String destinationName, String group); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/PollerConfigEnvironmentPostProcessor.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/PollerConfigEnvironmentPostProcessor.java deleted file mode 100644 index 6e1fc8c7d..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/PollerConfigEnvironmentPostProcessor.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2021-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.cloud.stream.config; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; - -/** - * - * @author Artem Bilan - * - * @since 3.2 - */ -public class PollerConfigEnvironmentPostProcessor implements EnvironmentPostProcessor { - - private static final Log logger = LogFactory.getLog(PollerConfigEnvironmentPostProcessor.class); - - private static final String STREAM_PROPERTY_PREFIX = "spring.cloud.stream.poller."; - - private static final String INTEGRATION_PROPERTY_PREFIX = "spring.integration.poller."; - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - Map streamPollerProperties = new HashMap<>(); - - String[] pollerPropertySuffixes = { "fixed-delay", "max-messages-per-poll", "cron", "initial-delay" }; - - PropertyMapper map = PropertyMapper.get().alwaysApplying(PropertyMapper.Source::whenHasText); - - for (String pollerPropertySuffix : pollerPropertySuffixes) { - map.from(environment.getProperty(STREAM_PROPERTY_PREFIX + pollerPropertySuffix)) - .to((value) -> streamPollerProperties.put(INTEGRATION_PROPERTY_PREFIX + pollerPropertySuffix, value)); - } - - if (!streamPollerProperties.isEmpty()) { - logger.info("'spring.cloud.stream.poller' " + - "properties are deprecated in favor of 'spring.integration.poller' properties."); - } - - //TODO Must remain after removal of deprecated code above in the future - if (!streamPollerProperties.containsKey(INTEGRATION_PROPERTY_PREFIX + "cron") && - !environment.containsProperty(INTEGRATION_PROPERTY_PREFIX + "cron") && - !environment.containsProperty(INTEGRATION_PROPERTY_PREFIX + "fixed-rate") && - !environment.containsProperty(INTEGRATION_PROPERTY_PREFIX + "fixed-delay")) { - - streamPollerProperties.putIfAbsent(INTEGRATION_PROPERTY_PREFIX + "fixed-delay", "1s"); - } - if (!environment.containsProperty(INTEGRATION_PROPERTY_PREFIX + "max-messages-per-poll")) { - streamPollerProperties.putIfAbsent(INTEGRATION_PROPERTY_PREFIX + "max-messages-per-poll", "1"); - } - - if (!streamPollerProperties.isEmpty()) { - environment.getPropertySources() - .addLast(new MapPropertySource("spring.integration.poller", streamPollerProperties)); - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ProducerMessageHandlerCustomizer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ProducerMessageHandlerCustomizer.java deleted file mode 100644 index 548a48d40..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/ProducerMessageHandlerCustomizer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.config; - -import org.springframework.messaging.MessageHandler; - -/** - * If a single bean of this type is in the application context, a producing message - * handler created by the binder can be further customized after all the properties are - * set. For example, to configure less-common properties. - * - * @param {@link MessageHandler} type - * - * @author Artem Bilan - * - * @since 3.0 - */ -@FunctionalInterface -public interface ProducerMessageHandlerCustomizer { - - /** - * Configure a {@link MessageHandler} that is being created by the binder for the - * provided destination name. - * @param handler the {@link MessageHandler} from the binder. - * @param destinationName the bound destination name. - */ - void configure(H handler, String destinationName); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SmartMessageMethodArgumentResolver.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SmartMessageMethodArgumentResolver.java deleted file mode 100644 index 102825246..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SmartMessageMethodArgumentResolver.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.config; - -import java.lang.reflect.Type; - -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.converter.MessageConversionException; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.converter.SmartMessageConverter; -import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; -import org.springframework.messaging.handler.annotation.support.MethodArgumentTypeMismatchException; -import org.springframework.messaging.support.ErrorMessage; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; - -/** - * @author Oleg Zhurakousky - */ -class SmartMessageMethodArgumentResolver extends MessageMethodArgumentResolver { - - private final MessageConverter messageConverter; - - SmartMessageMethodArgumentResolver() { - this(null); - } - - /** - * Create a resolver instance with the given {@link MessageConverter}. - * @param converter the MessageConverter to use (may be {@code null}) - * @since 4.3 - */ - SmartMessageMethodArgumentResolver(@Nullable MessageConverter converter) { - this.messageConverter = converter; - } - - @Override - public Object resolveArgument(MethodParameter parameter, Message message) - throws Exception { - Class targetMessageType = parameter.getParameterType(); - Class targetPayloadType = getPayloadType(parameter); - - if (!targetMessageType.isAssignableFrom(message.getClass())) { - throw new MethodArgumentTypeMismatchException(message, parameter, - "Actual message type '" + ClassUtils.getDescriptiveType(message) - + "' does not match expected type '" - + ClassUtils.getQualifiedName(targetMessageType) + "'"); - } - - Class payloadClass = message.getPayload().getClass(); - - if (message instanceof ErrorMessage - || conversionNotRequired(payloadClass, targetPayloadType)) { - return message; - } - Object payload = message.getPayload(); - if (isEmptyPayload(payload)) { - if (isExplicitNullPayload(payload)) { - return message; - } - throw new MessageConversionException(message, - "Cannot convert from actual payload type '" - + ClassUtils.getDescriptiveType(payload) - + "' to expected payload type '" - + ClassUtils.getQualifiedName(targetPayloadType) - + "' when payload is empty"); - } - - payload = convertPayload(message, parameter, targetPayloadType); - return MessageBuilder.createMessage(payload, message.getHeaders()); - } - - private boolean conversionNotRequired(Class a, Class b) { - return b == Object.class - ? ClassUtils.isAssignable(a, b) : ClassUtils.isAssignable(b, a); - } - - private Class getPayloadType(MethodParameter parameter) { - Type genericParamType = parameter.getGenericParameterType(); - ResolvableType resolvableType = ResolvableType.forType(genericParamType) - .as(Message.class); - return resolvableType.getGeneric().toClass(); - } - - @Override - protected boolean isEmptyPayload(@Nullable Object payload) { - if (payload == null) { - return true; - } - else if (payload instanceof byte[]) { - return ((byte[]) payload).length == 0; - } - else if (payload instanceof String) { - return !StringUtils.hasText((String) payload); - } - else { - return "org.springframework.kafka.support.KafkaNull".equals(payload.getClass().getName()); - } - } - - protected boolean isExplicitNullPayload(@Nullable Object payload) { - return "org.springframework.kafka.support.KafkaNull" - .equals(payload.getClass().getName()); - } - - private Object convertPayload(Message message, MethodParameter parameter, - Class targetPayloadType) { - Object result = null; - if (this.messageConverter instanceof SmartMessageConverter) { - SmartMessageConverter smartConverter = (SmartMessageConverter) this.messageConverter; - result = smartConverter.fromMessage(message, targetPayloadType, parameter); - } - else if (this.messageConverter != null) { - result = this.messageConverter.fromMessage(message, targetPayloadType); - } - - if (result == null) { - throw new MessageConversionException(message, - "No converter found from actual payload type '" - + ClassUtils.getDescriptiveType(message.getPayload()) - + "' to expected payload type '" - + ClassUtils.getQualifiedName(targetPayloadType) + "'"); - } - return result; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SmartPayloadArgumentResolver.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SmartPayloadArgumentResolver.java deleted file mode 100644 index 4d2edc0fc..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SmartPayloadArgumentResolver.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2016-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.cloud.stream.config; - -import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.MessageConversionException; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.converter.SmartMessageConverter; -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.handler.annotation.support.MethodArgumentNotValidException; -import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.Validator; - -/** - * @author Oleg Zhurakousky - * @author Gary Russell - */ -class SmartPayloadArgumentResolver extends PayloadMethodArgumentResolver { - - private final MessageConverter messageConverter; - - SmartPayloadArgumentResolver(MessageConverter messageConverter) { - super(messageConverter); - this.messageConverter = messageConverter; - } - - SmartPayloadArgumentResolver(MessageConverter messageConverter, Validator validator) { - super(messageConverter, validator, true); - this.messageConverter = messageConverter; - } - - SmartPayloadArgumentResolver(MessageConverter messageConverter, Validator validator, - boolean useDefaultResolution) { - super(messageConverter, validator, useDefaultResolution); - this.messageConverter = messageConverter; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return (!Message.class.isAssignableFrom(parameter.getParameterType()) - && !MessageHeaders.class.isAssignableFrom(parameter.getParameterType()) - && !parameter.hasParameterAnnotation(Header.class) - && !parameter.hasParameterAnnotation(Headers.class)); - } - - /** - * Needed to support mapping KafkaNull to null in method invocation. - */ - @Override - protected boolean isEmptyPayload(Object payload) { - return super.isEmptyPayload(payload) - || "org.springframework.kafka.support.KafkaNull" - .equals(payload.getClass().getName()); - } - - @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message) - throws Exception { - Payload ann = parameter.getParameterAnnotation(Payload.class); - if (ann != null && StringUtils.hasText(ann.expression())) { - throw new IllegalStateException( - "@Payload SpEL expressions not supported by this resolver"); - } - - Object payload = message.getPayload(); - if (isEmptyPayload(payload)) { - if (ann == null || ann.required()) { - String paramName = getParameterName(parameter); - BindingResult bindingResult = new BeanPropertyBindingResult(payload, - paramName); - bindingResult.addError( - new ObjectError(paramName, "Payload value must not be empty")); - throw new MethodArgumentNotValidException(message, parameter, - bindingResult); - } - else { - return null; - } - } - - Class targetClass = parameter.getParameterType(); - Class payloadClass = payload.getClass(); - if (conversionNotRequired(payloadClass, targetClass)) { - validate(message, parameter, payload); - return payload; - } - else { - if (this.messageConverter instanceof SmartMessageConverter) { - SmartMessageConverter smartConverter = (SmartMessageConverter) this.messageConverter; - payload = smartConverter.fromMessage(message, targetClass, parameter); - } - else { - payload = this.messageConverter.fromMessage(message, targetClass); - } - if (payload == null) { - throw new MessageConversionException(message, - "Cannot convert from [" + payloadClass.getName() + "] to [" - + targetClass.getName() + "] for " + message); - } - validate(message, parameter, payload); - return payload; - } - } - - private boolean conversionNotRequired(Class a, Class b) { - return b == Object.class - ? ClassUtils.isAssignable(a, b) : ClassUtils.isAssignable(b, a); - } - - private String getParameterName(MethodParameter param) { - String paramName = param.getParameterName(); - return (paramName != null ? paramName : "Arg " + param.getParameterIndex()); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SpelExpressionConverterConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SpelExpressionConverterConfiguration.java deleted file mode 100644 index 4b90f7680..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SpelExpressionConverterConfiguration.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2015-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import java.beans.Introspector; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.annotation.Role; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.ParseException; -import org.springframework.expression.spel.standard.SpelExpression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.integration.config.IntegrationConverter; -import org.springframework.integration.context.IntegrationContextUtils; -import org.springframework.integration.expression.SpelPropertyAccessorRegistrar; -import org.springframework.integration.json.JsonPropertyAccessor; - -/** - * Adds a Converter from String to SpEL Expression in the context. - * - * @author Eric Bottard - * @author Artem Bilan - */ -@Configuration(proxyBeanMethods = false) -@Role(BeanDefinition.ROLE_INFRASTRUCTURE) -public class SpelExpressionConverterConfiguration { - - /** - * Provide a {@link SpelPropertyAccessorRegistrar} supplied with the - * {@link JsonPropertyAccessor}. This bean is used - * to customize an - * {@link org.springframework.integration.config.IntegrationEvaluationContextFactoryBean}. - * for additional {@link org.springframework.expression.PropertyAccessor}s. - * @return the SpelPropertyAccessorRegistrar bean - * @see org.springframework.integration.config.IntegrationEvaluationContextFactoryBean - */ - @Bean - public static SpelPropertyAccessorRegistrar spelPropertyAccessorRegistrar() { - return new SpelPropertyAccessorRegistrar() - .add(Introspector - .decapitalize(JsonPropertyAccessor.class.getSimpleName()), - new JsonPropertyAccessor()); - } - - @Bean - @ConfigurationPropertiesBinding - @IntegrationConverter - public Converter spelConverter(ConfigurableApplicationContext context) { - SpelConverter converter = new SpelConverter(); - ConfigurableConversionService cs = (ConfigurableConversionService) context.getBeanFactory().getConversionService(); - if (cs != null) { - cs.addConverter(converter); - } - return converter; - } - - /** - * A simple converter from String to Expression. - * - * @author Eric Bottard - */ - public static class SpelConverter implements Converter { - - private SpelExpressionParser parser = new SpelExpressionParser(); - - @Autowired - @Qualifier(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME) - @Lazy - private EvaluationContext evaluationContext; - - @Override - public Expression convert(String source) { - try { - Expression expression = this.parser.parseExpression(source); - if (expression instanceof SpelExpression) { - ((SpelExpression) expression) - .setEvaluationContext(this.evaluationContext); - } - return expression; - } - catch (ParseException e) { - throw new IllegalArgumentException(String.format( - "Could not convert '%s' into a SpEL expression", source), e); - } - } - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SpringIntegrationProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SpringIntegrationProperties.java deleted file mode 100644 index aa8754967..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/SpringIntegrationProperties.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.messaging.MessageHeaders; - -/** - * Contains properties for Spring Integration settings. - * - * @author Marius Bogoevici - * @since 1.2.3 - */ -@ConfigurationProperties("spring.cloud.stream.integration") -public class SpringIntegrationProperties { - - /** - * Message header names that will NOT be copied from the inbound message. - */ - private String[] messageHandlerNotPropagatedHeaders = new String[] { - MessageHeaders.CONTENT_TYPE }; - - public String[] getMessageHandlerNotPropagatedHeaders() { - return this.messageHandlerNotPropagatedHeaders; - } - - public void setMessageHandlerNotPropagatedHeaders( - String[] messageHandlerNotPropagatedHeaders) { - this.messageHandlerNotPropagatedHeaders = messageHandlerNotPropagatedHeaders; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/CompositeMessageConverterFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/CompositeMessageConverterFactory.java deleted file mode 100644 index 195fc9dce..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/CompositeMessageConverterFactory.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.converter; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.function.context.config.JsonMessageConverter; -import org.springframework.cloud.function.json.JacksonMapper; -import org.springframework.cloud.function.json.JsonMapper; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.lang.Nullable; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.AbstractMessageConverter; -import org.springframework.messaging.converter.ByteArrayMessageConverter; -import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.converter.DefaultContentTypeResolver; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.util.CollectionUtils; -import org.springframework.util.MimeType; -/** - * A factory for creating an instance of {@link CompositeMessageConverter} for a given - * target MIME type. - * - * @author David Turanski - * @author Ilayaperumal Gopinathan - * @author Marius Bogoevici - * @author Vinicius Carvalho - * @author Oleg Zhurakousky - */ -public class CompositeMessageConverterFactory { - - private final Log log = LogFactory.getLog(CompositeMessageConverterFactory.class); - - private final ObjectMapper objectMapper; - - private final List converters; - - private final JsonMapper jsonMapper; - - public CompositeMessageConverterFactory() { - this(Collections.emptyList(), new ObjectMapper(), null); - } - - /** - * @param customConverters a list of {@link AbstractMessageConverter} - * @param objectMapper object mapper for for serialization / deserialization - */ - public CompositeMessageConverterFactory( - List customConverters, - ObjectMapper objectMapper, JsonMapper jsonMapper) { - this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper; - this.jsonMapper = jsonMapper == null ? new JacksonMapper(this.objectMapper) : jsonMapper; - if (!CollectionUtils.isEmpty(customConverters)) { - this.converters = new ArrayList<>(customConverters); - } - else { - this.converters = new ArrayList<>(); - } - initDefaultConverters(); - - DefaultContentTypeResolver resolver = new DefaultContentTypeResolver() { - @Override - public MimeType resolve(@Nullable MessageHeaders headers) { - Map messageHeaders = new HashMap<>(headers); - Object contentType = headers.get(MessageHeaders.CONTENT_TYPE); - if (contentType instanceof byte[]) { - contentType = new String((byte[]) contentType, StandardCharsets.UTF_8); - contentType = ((String) contentType).replace("\"", ""); - messageHeaders.put(MessageHeaders.CONTENT_TYPE, contentType); - } - return super.resolve(new MessageHeaders(messageHeaders)); - } - }; - resolver.setDefaultMimeType(BindingProperties.DEFAULT_CONTENT_TYPE); - this.converters.stream().filter(mc -> mc instanceof AbstractMessageConverter) - .forEach(mc -> ((AbstractMessageConverter) mc) - .setContentTypeResolver(resolver)); - } - - private void initDefaultConverters() { - this.converters.add(new JsonMessageConverter(this.jsonMapper) { - @Override - protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, - @Nullable Object conversionHint) { - /* - * We must revisit this. This is a copy from ApplicationMarshallingMessageConverter which derived from an older class etc. . . - * This attempts to use JSON conversion to convert something that is not json in the first place. - * For example Integer payload with application/json CT should actually fail since Integer is not a JSON. - * This is !!!!wrong!!!!! and ONLY remains here for backward compatibility. - */ - if (payload instanceof String) { - return ((String) payload).getBytes(StandardCharsets.UTF_8); - } - return super.convertToInternal(payload, headers, conversionHint); - } - }); - this.converters.add(new ByteArrayMessageConverter() { - @Override - protected boolean supports(Class clazz) { - if (!super.supports(clazz)) { - return (Object.class == clazz); - } - return true; - } - }); - this.converters.add(new ObjectStringMessageConverter()); - } - - /** - * Determine the JSON encoding to use for the given content type. - * @param contentType the MIME type from the MessageHeaders, if any - * @return the JSON encoding to use (never {@code null}) - */ - private JsonEncoding getJsonEncoding(@Nullable MimeType contentType) { - if (contentType != null && contentType.getCharset() != null) { - Charset charset = contentType.getCharset(); - for (JsonEncoding encoding : JsonEncoding.values()) { - if (charset.name().equals(encoding.getJavaName())) { - return encoding; - } - } - } - return JsonEncoding.UTF8; - } - - /** - * Creation method. - * @param mimeType the target MIME type - * @return a converter for the target MIME type - */ - public MessageConverter getMessageConverterForType(MimeType mimeType) { - List converters = new ArrayList<>(); - for (MessageConverter converter : this.converters) { - if (converter instanceof AbstractMessageConverter) { - for (MimeType type : ((AbstractMessageConverter) converter) - .getSupportedMimeTypes()) { - if (type.includes(mimeType)) { - converters.add(converter); - } - } - } - else { - if (this.log.isDebugEnabled()) { - this.log.debug("Ommitted " + converter + " of type " - + converter.getClass().toString() + " for '" - + mimeType.toString() - + "' as it is not an AbstractMessageConverter"); - } - } - } - if (CollectionUtils.isEmpty(converters)) { - throw new ConversionException( - "No message converter is registered for " + mimeType.toString()); - } - if (converters.size() > 1) { - return new CompositeMessageConverter(converters); - } - else { - return converters.get(0); - } - } - - public CompositeMessageConverter getMessageConverterForAllRegistered() { - return new CompositeMessageConverter(new ArrayList<>(this.converters)); - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ConversionException.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ConversionException.java deleted file mode 100644 index 5746f4267..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ConversionException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.converter; - -/** - * Exception thrown when an error is encountered during message conversion. - * - * @author David Turanski - */ -@SuppressWarnings("serial") -public class ConversionException extends RuntimeException { - - public ConversionException(String message) { - super(message); - } - - public ConversionException(String message, Throwable t) { - super(message, t); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/CustomMimeTypeConverter.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/CustomMimeTypeConverter.java deleted file mode 100644 index a57c36a9d..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/CustomMimeTypeConverter.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2013-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.converter; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.util.MimeType; - -/** - * A custom converter for {@link MimeType} that accepts a plain java class name as a - * shorthand for {@code application/x-java-object;type=the.qualified.ClassName}. - * - * @author Eric Bottard - * @author David Turanski - */ -public class CustomMimeTypeConverter implements Converter { - - @Override - public MimeType convert(String source) { - if (!source.contains("/")) { - return MimeType.valueOf("application/x-java-object;type=" + source); - } - return MimeType.valueOf(source); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/MessageConverterUtils.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/MessageConverterUtils.java deleted file mode 100644 index 4f9bf0880..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/MessageConverterUtils.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2015-2016 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.converter; - -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.MimeType; -import org.springframework.util.StringUtils; - -/** - * Message conversion utility methods. - * - * @author David Turanski - * @author Gary Russell - * @author Ilayaperumal Gopinathan - */ -public abstract class MessageConverterUtils { - - /** - * A general MimeType for Java Types. - */ - public static final MimeType X_JAVA_OBJECT = MimeType - .valueOf("application/x-java-object"); - - /** - * A general MimeType for a Java serialized byte array. - */ - public static final MimeType X_JAVA_SERIALIZED_OBJECT = MimeType - .valueOf("application/x-java-serialized-object"); - - /** - * Get the java Object type for the MimeType X_JAVA_OBJECT. - * @param contentType content type - * @return the class for the content type. - */ - public static Class getJavaTypeForJavaObjectContentType(MimeType contentType) { - Assert.isTrue(X_JAVA_OBJECT.includes(contentType), "Content type must be " - + X_JAVA_OBJECT.toString() + ", or " + "included in it"); - if (contentType.getParameter("type") != null) { - try { - return ClassUtils.forName(contentType.getParameter("type"), null); - } - catch (Exception e) { - throw new ConversionException(e.getMessage(), e); - } - } - return Object.class; - } - - /** - * Build the conventional {@link MimeType} for a java object. - * @param clazz the java type - * @return the MIME type - */ - public static MimeType javaObjectMimeType(Class clazz) { - return MimeType.valueOf("application/x-java-object;type=" + clazz.getName()); - } - - public static MimeType getMimeType(String contentTypeString) { - MimeType mimeType = null; - if (StringUtils.hasText(contentTypeString)) { - try { - mimeType = resolveContentType(contentTypeString); - } - catch (ClassNotFoundException cfe) { - throw new IllegalArgumentException( - "Could not find the class required for " + contentTypeString, - cfe); - } - } - return mimeType; - } - - public static MimeType resolveContentType(String type) - throws ClassNotFoundException, LinkageError { - if (!type.contains("/")) { - Class javaType = resolveJavaType(type); - return javaObjectMimeType(javaType); - } - return MimeType.valueOf(type); - } - - public static Class resolveJavaType(String type) - throws ClassNotFoundException, LinkageError { - return ClassUtils.forName(type, null); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ObjectStringMessageConverter.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ObjectStringMessageConverter.java deleted file mode 100644 index 4d24384ff..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ObjectStringMessageConverter.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.converter; - -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.stream.Collectors; - -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.AbstractMessageConverter; -import org.springframework.util.MimeType; - -/** - * A {@link org.springframework.messaging.converter.MessageConverter} to convert a - * non-String objects to a String, when expected content type is "text/plain". - * - * It only performs conversions to internal format and is a wrapper around - * {@link Object#toString()}. - * - * @author Marius Bogoevici - * @author Oleg Zhurakousky - * @author Byungjun You - * @since 1.2 - */ -public class ObjectStringMessageConverter extends AbstractMessageConverter { - - public ObjectStringMessageConverter() { - super(new MimeType("text", "*", StandardCharsets.UTF_8)); - setStrictContentTypeMatch(true); - } - - @Override - protected boolean supports(Class clazz) { - return true; - } - - @Override - protected boolean canConvertFrom(Message message, Class targetClass) { - // only supports the conversion to String - return supportsMimeType(message.getHeaders()); - } - - @Override - protected boolean supportsMimeType(@Nullable MessageHeaders headers) { - MimeType mimeType = getMimeType(headers); - if (mimeType != null) { - for (MimeType current : getSupportedMimeTypes()) { - if (current.getType().equals(mimeType.getType())) { - return true; - } - } - } - - return super.supportsMimeType(headers); - } - - @Override - protected Object convertFromInternal(Message message, Class targetClass, Object conversionHint) { - -// Assert.isTrue(String.class.isAssignableFrom(targetClass) || targetClass == Object.class, "This converter can only convert byte[] to String"); - if (message.getPayload() != null) { - if (message.getPayload() instanceof byte[] payloadAsBytes) { - if (byte[].class.isAssignableFrom(targetClass)) { - return message.getPayload(); - } - else { - return new String(payloadAsBytes, - StandardCharsets.UTF_8); - } - } - else if (message.getPayload() instanceof Collection payloadAsCollection) { - return payloadAsCollection.stream() - .map(value -> { - if (byte[].class.isAssignableFrom(targetClass)) { - return value; - } - else if (value instanceof byte[] valueAsBytes) { - return new String(valueAsBytes, StandardCharsets.UTF_8); - } - else { - return value; // String - } - }).collect(Collectors.toList()); - } - else { - if (byte[].class.isAssignableFrom(targetClass)) { - return message.getPayload().toString() - .getBytes(StandardCharsets.UTF_8); - } - else { - return message.getPayload(); - } - } - } - return null; - } - - @Override - protected Object convertToInternal(Object payload, MessageHeaders headers, - Object conversionHint) { - if (payload != null) { - if ((payload instanceof byte[])) { - return payload; - } - else { - return payload.toString().getBytes(); - } - } - else { - return null; - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/endpoint/BindingsEndpoint.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/endpoint/BindingsEndpoint.java deleted file mode 100644 index 5f4238d00..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/endpoint/BindingsEndpoint.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.endpoint; - -import java.util.List; - -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.endpoint.annotation.Selector; -import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; -import org.springframework.cloud.stream.binder.Binding; -import org.springframework.cloud.stream.binding.BindingsLifecycleController; -import org.springframework.cloud.stream.binding.BindingsLifecycleController.State; - -/** - * - * Actuator endpoint for binding control. - * - * @author Oleg Zhurakousky - * @since 2.0 - */ -@Endpoint(id = "bindings") -public class BindingsEndpoint { - - private final BindingsLifecycleController lifecycleController; - - public BindingsEndpoint(BindingsLifecycleController lifecycleController) { - this.lifecycleController = lifecycleController; - } - - @WriteOperation - public void changeState(@Selector String name, State state) { - this.lifecycleController.changeState(name, state); - } - - @ReadOperation - public List queryStates() { - return this.lifecycleController.queryStates(); - } - - @ReadOperation - public List> queryState(@Selector String name) { - return this.lifecycleController.queryState(name); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/endpoint/ChannelsEndpoint.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/endpoint/ChannelsEndpoint.java deleted file mode 100644 index 796580bee..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/endpoint/ChannelsEndpoint.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.endpoint; - -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.beans.BeansException; -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.cloud.stream.binding.Bindable; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -/** - * An {@link Endpoint} that has the binding information on all the {@link Bindable} - * message channels. - * - * @author Dave Syer - * @author Ilayaperumal Gopinathan - * @author Vinicius Carvalho - * @author Oleg Zhurakousky - */ -@Endpoint(id = "channels") -public class ChannelsEndpoint implements ApplicationContextAware { - - private ApplicationContext applicationContext; - - private final BindingServiceProperties properties; - - public ChannelsEndpoint(BindingServiceProperties properties) { - this.properties = properties; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - @ReadOperation - public Map channels() { - Collection adapters = applicationContext.getBeansOfType(Bindable.class).values(); - ChannelsMetaData map = new ChannelsMetaData(); - Map inputs = map.getInputs(); - Map outputs = map.getOutputs(); - for (Bindable factory : adapters) { - for (String name : factory.getInputs()) { - inputs.put(name, this.properties.getBindingProperties(name)); - } - for (String name : factory.getOutputs()) { - outputs.put(name, this.properties.getBindingProperties(name)); - } - } - return new ObjectMapper().convertValue(map, - new TypeReference>() { - }); - } - - /** - * Meta data for channels. Contains e.g. input and outputs. - */ - @JsonInclude(Include.NON_DEFAULT) - public static class ChannelsMetaData { - - private Map inputs = new LinkedHashMap<>(); - - private Map outputs = new LinkedHashMap<>(); - - public Map getInputs() { - return this.inputs; - } - - public Map getOutputs() { - return this.outputs; - } - - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/BindableFunctionProxyFactory.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/BindableFunctionProxyFactory.java deleted file mode 100644 index 28efc5ed0..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/BindableFunctionProxyFactory.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.cloud.stream.binder.PollableMessageSource; -import org.springframework.cloud.stream.binding.BindableProxyFactory; -import org.springframework.cloud.stream.binding.BoundTargetHolder; -import org.springframework.cloud.stream.binding.SupportedBindableFeatures; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; - -/** - * {@link FactoryBean} for creating inputs/outputs destinations to be bound to - * function arguments. It is an extension to {@link BindableProxyFactory} which - * operates on Bindable interfaces (e.g., Source, Processor, Sink) which internally - * define inputs and output channels. Unlike BindableProxyFactory, this class - * operates based on the count of provided inputs and outputs deriving the binding - * (channel) names based on convention - {@code `. + + .`} - *
- * For example, `myFunction.in.0` - is the binding for the first input argument of the - * function with the name `myFunction`. - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * - * @since 3.0 - */ -public class BindableFunctionProxyFactory extends BindableProxyFactory implements ApplicationContextAware { - - private final int inputCount; - - private final int outputCount; - - private final String functionDefinition; - - private final StreamFunctionProperties functionProperties; - - private final SupportedBindableFeatures supportedBindableFeatures; - - private final boolean functionExist; - - private GenericApplicationContext context; - - public BindableFunctionProxyFactory(String functionDefinition, int inputCount, int outputCount, StreamFunctionProperties functionProperties) { - this(functionDefinition, inputCount, outputCount, functionProperties, new SupportedBindableFeatures(), true); - } - - BindableFunctionProxyFactory(String functionDefinition, int inputCount, int outputCount, StreamFunctionProperties functionProperties, boolean functionExist) { - this(functionDefinition, inputCount, outputCount, functionProperties, new SupportedBindableFeatures(), functionExist); - } - - BindableFunctionProxyFactory(String functionDefinition, int inputCount, int outputCount, StreamFunctionProperties functionProperties, SupportedBindableFeatures supportedBindableFeatures) { - this(functionDefinition, inputCount, outputCount, functionProperties, supportedBindableFeatures, true); - } - - BindableFunctionProxyFactory(String functionDefinition, int inputCount, int outputCount, StreamFunctionProperties functionProperties, - SupportedBindableFeatures supportedBindableFeatures, boolean functionExist) { - super(null); - this.inputCount = inputCount; - this.outputCount = outputCount; - this.functionDefinition = functionDefinition; - this.functionProperties = functionProperties; - this.supportedBindableFeatures = supportedBindableFeatures; - this.functionExist = functionExist; - } - - @Override - public void afterPropertiesSet() { - populateBindingTargetFactories(beanFactory); - Assert.notEmpty(BindableFunctionProxyFactory.this.bindingTargetFactories, - "'bindingTargetFactories' cannot be empty"); - - if (this.inputCount > 0) { - for (int i = 0; i < inputCount; i++) { - this.createInput(this.buildInputNameForIndex(i)); - } - } - - if (this.outputCount > 0) { - for (int i = 0; i < outputCount; i++) { - this.createOutput(this.buildOutputNameForIndex(i)); - } - } - } - - @Override - public Class getObjectType() { - return this.type; - } - - @Override - public boolean isSingleton() { - return true; - } - - protected String getFunctionDefinition() { - return this.isFunctionExist() ? this.functionDefinition : null; - } - - protected String getInputName(int index) { - return CollectionUtils.isEmpty(this.getInputs()) - ? null - : this.getInputs().toArray(new String[0])[index]; - } - - protected String getOutputName(int index) { - String outputName = null; - if (this.outputCount > 0) { - outputName = CollectionUtils.isEmpty(this.getOutputs()) - ? null - : this.getOutputs().toArray(new String[0])[index]; - } - return outputName; - } - - protected boolean isMultiple() { - return this.inputCount > 1 || this.outputCount > 1; - } - - private String buildInputNameForIndex(int index) { - if (!this.isFunctionExist()) { - return this.functionDefinition; - } - return new StringBuilder(this.functionDefinition.replace(",", "|").replace("|", "")) - .append(FunctionConstants.DELIMITER) - .append(FunctionConstants.DEFAULT_INPUT_SUFFIX) - .append(FunctionConstants.DELIMITER) - .append(index) - .toString(); - } - - private String buildOutputNameForIndex(int index) { - if (!this.isFunctionExist()) { - return this.functionDefinition; - } - return new StringBuilder(this.functionDefinition.replace(",", "|").replace("|", "")) - .append(FunctionConstants.DELIMITER) - .append(FunctionConstants.DEFAULT_OUTPUT_SUFFIX) - .append(FunctionConstants.DELIMITER) - .append(index) - .toString(); - } - - private void createInput(String name) { - if (this.functionProperties.getBindings().containsKey(name)) { - name = this.functionProperties.getBindings().get(name); - } - if (this.supportedBindableFeatures.isPollable()) { - PollableMessageSource pollableSource = (PollableMessageSource) getBindingTargetFactory(PollableMessageSource.class).createInput(name); - if (context != null && !context.containsBean(name)) { - context.registerBean(name, PollableMessageSource.class, () -> pollableSource); - } - this.inputHolders.put(name, new BoundTargetHolder(pollableSource, true)); - } - else if (this.supportedBindableFeatures.isReactive()) { - this.inputHolders.put(name, - new BoundTargetHolder(getBindingTargetFactory(FluxMessageChannel.class) - .createInput(name), true)); - } - else { - this.inputHolders.put(name, - new BoundTargetHolder(getBindingTargetFactory(SubscribableChannel.class) - .createInput(name), true)); - } - } - - private void createOutput(String name) { - if (this.functionProperties.getBindings().containsKey(name)) { - name = this.functionProperties.getBindings().get(name); - } - if (this.supportedBindableFeatures.isReactive()) { - this.outputHolders.put(name, - new BoundTargetHolder(getBindingTargetFactory(FluxMessageChannel.class) - .createOutput(name), true)); - } - else { - this.outputHolders.put(name, - new BoundTargetHolder(getBindingTargetFactory(SubscribableChannel.class) - .createOutput(name), true)); - } - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.context = (GenericApplicationContext) applicationContext; - } - - public boolean isFunctionExist() { - return functionExist; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java deleted file mode 100644 index 0d46a5a32..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConfiguration.java +++ /dev/null @@ -1,1047 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.lang.reflect.Field; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.StreamSupport; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; -import reactor.util.function.Tuples; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.FunctionRegistry; -import org.springframework.cloud.function.context.PollableBean; -import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; -import org.springframework.cloud.function.context.config.FunctionContextUtils; -import org.springframework.cloud.function.context.config.RoutingFunction; -import org.springframework.cloud.function.context.message.MessageUtils; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.BindingCreatedEvent; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties.PollerProperties; -import org.springframework.cloud.stream.binding.BindableProxyFactory; -import org.springframework.cloud.stream.binding.DefaultPartitioningInterceptor; -import org.springframework.cloud.stream.binding.NewDestinationBindingCallback; -import org.springframework.cloud.stream.binding.SupportedBindableFeatures; -import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.EnvironmentAware; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.type.MethodMetadata; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.channel.AbstractSubscribableChannel; -import org.springframework.integration.channel.FluxMessageChannel; -import org.springframework.integration.core.MessagingTemplate; -import org.springframework.integration.dsl.IntegrationFlow; -import org.springframework.integration.dsl.IntegrationFlowBuilder; -import org.springframework.integration.expression.ExpressionUtils; -import org.springframework.integration.handler.AbstractMessageHandler; -import org.springframework.integration.scheduling.PollerMetadata; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.integration.util.IntegrationReactiveUtils; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.Trigger; -import org.springframework.scheduling.support.CronTrigger; -import org.springframework.scheduling.support.PeriodicTrigger; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * @author Oleg Zhurakousky - * @author David Turanski - * @author Ilayaperumal Gopinathan - * @author Soby Chacko - * @author Chris Bono - * @author Byungjun You - * @author Ivan Shapoval - * @since 2.1 - */ -@AutoConfiguration -@EnableConfigurationProperties(StreamFunctionConfigurationProperties.class) -@Import({ BinderFactoryAutoConfiguration.class }) -@AutoConfigureBefore(BindingServiceConfiguration.class) -@AutoConfigureAfter(ContextFunctionCatalogAutoConfiguration.class) -@ConditionalOnBean(FunctionRegistry.class) -public class FunctionConfiguration { - - @SuppressWarnings("rawtypes") - @Bean - public StreamBridge streamBridgeUtils(FunctionCatalog functionCatalog, - BindingServiceProperties bindingServiceProperties, ConfigurableApplicationContext applicationContext, - @Nullable NewDestinationBindingCallback callback) { - return new StreamBridge(functionCatalog, bindingServiceProperties, applicationContext, callback); - } - - @Bean - public InitializingBean functionBindingRegistrar(Environment environment, FunctionCatalog functionCatalog, - StreamFunctionProperties streamFunctionProperties) { - return new FunctionBindingRegistrar(functionCatalog, streamFunctionProperties); - } - - @Bean - public InitializingBean functionInitializer(FunctionCatalog functionCatalog, - StreamFunctionProperties functionProperties, - BindingServiceProperties serviceProperties, ConfigurableApplicationContext applicationContext, - StreamBridge streamBridge) { - - boolean shouldCreateInitializer = true; - return shouldCreateInitializer - ? new FunctionToDestinationBinder(functionCatalog, functionProperties, - serviceProperties, streamBridge) - : null; - } - - /* - * Binding initializer responsible only for Suppliers - */ - @Bean - InitializingBean supplierInitializer(FunctionCatalog functionCatalog, StreamFunctionProperties functionProperties, - GenericApplicationContext context, BindingServiceProperties serviceProperties, - @Nullable List proxyFactories, StreamBridge streamBridge, - TaskScheduler taskScheduler) { - - if (CollectionUtils.isEmpty(proxyFactories)) { - return null; - } - - return new InitializingBean() { - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public void afterPropertiesSet() throws Exception { - for (BindableFunctionProxyFactory proxyFactory : proxyFactories) { - FunctionInvocationWrapper functionWrapper = functionCatalog.lookup(proxyFactory.getFunctionDefinition()); - if (functionWrapper != null && functionWrapper.isSupplier()) { - // gather output content types - List contentTypes = new ArrayList(); - if (proxyFactory.getOutputs().size() == 0) { - return; - } - Assert.isTrue(proxyFactory.getOutputs().size() == 1, "Supplier with multiple outputs is not supported at the moment."); - String outputName = proxyFactory.getOutputs().iterator().next(); - - BindingProperties bindingProperties = serviceProperties.getBindingProperties(outputName); - ProducerProperties producerProperties = bindingProperties.getProducer(); - if (!(bindingProperties.getProducer() != null && producerProperties.isUseNativeEncoding())) { - contentTypes.add(bindingProperties.getContentType()); - } - - // see https://github.com/spring-cloud/spring-cloud-stream/issues/2027 - String functionDefinition = proxyFactory.getFunctionDefinition(); - if (!StringUtils.hasText(functionDefinition)) { - continue; - } - String[] functionNames = StringUtils.delimitedListToStringArray(functionDefinition.replaceAll(",", "|").trim(), "|"); - - Function supplier = null; - Function function = null; - if (!ObjectUtils.isEmpty(functionNames) && functionNames.length > 1) { - String supplierName = functionNames[0]; - String remainingFunctionDefinition = StringUtils - .arrayToCommaDelimitedString(Arrays.copyOfRange(functionNames, 1, functionNames.length)); - - supplier = functionCatalog.lookup(supplierName); - function = functionCatalog.lookup(remainingFunctionDefinition, contentTypes.toArray(new String[0])); - - if (!((FunctionInvocationWrapper) supplier).isOutputTypePublisher() && - ((FunctionInvocationWrapper) function).isInputTypePublisher()) { - functionWrapper = null; - } - else { - functionWrapper = functionCatalog.lookup(proxyFactory.getFunctionDefinition(), contentTypes.toArray(new String[0])); - } - } - else { - functionWrapper = functionCatalog.lookup(proxyFactory.getFunctionDefinition(), contentTypes.toArray(new String[0])); - } - - Publisher beginPublishingTrigger = setupBindingTrigger(context); - - if (!functionProperties.isComposeFrom() && !functionProperties.isComposeTo()) { - String integrationFlowName = proxyFactory.getFunctionDefinition() + "_integrationflow"; - - PollableBean pollable = null; - try { - pollable = extractPollableAnnotation(functionProperties, context, proxyFactory); - } - catch (Exception e) { - // Will fix itself once https://github.com/spring-projects/spring-framework/issues/28748 is fixed - } - - if (functionWrapper != null) { - IntegrationFlow integrationFlow = integrationFlowFromProvidedSupplier(new PartitionAwareFunctionWrapper(functionWrapper, context, producerProperties), - beginPublishingTrigger, pollable, context, taskScheduler, producerProperties, outputName) - .route(Message.class, message -> { - if (message.getHeaders().get("spring.cloud.stream.sendto.destination") != null) { - String destinationName = (String) message.getHeaders().get("spring.cloud.stream.sendto.destination"); - return streamBridge.resolveDestination(destinationName, producerProperties, null); - } - return outputName; - }).get(); - IntegrationFlow postProcessedFlow = (IntegrationFlow) context.getAutowireCapableBeanFactory() - .applyBeanPostProcessorsBeforeInitialization(integrationFlow, integrationFlowName); - context.registerBean(integrationFlowName, IntegrationFlow.class, () -> postProcessedFlow); - } - else { - IntegrationFlow integrationFlow = integrationFlowFromProvidedSupplier(new PartitionAwareFunctionWrapper(supplier, context, producerProperties), - beginPublishingTrigger, pollable, context, taskScheduler, producerProperties, outputName) - .channel(c -> c.direct()) - .fluxTransform((Function>, ? extends Publisher>) function) - .route(Message.class, message -> { - if (message.getHeaders().get("spring.cloud.stream.sendto.destination") != null) { - String destinationName = (String) message.getHeaders().get("spring.cloud.stream.sendto.destination"); - return streamBridge.resolveDestination(destinationName, producerProperties, null); - } - return outputName; - }) - .get(); - IntegrationFlow postProcessedFlow = (IntegrationFlow) context.getAutowireCapableBeanFactory() - .applyBeanPostProcessorsBeforeInitialization(integrationFlow, integrationFlowName); - context.registerBean(integrationFlowName, IntegrationFlow.class, () -> postProcessedFlow); - } - } - } - } - } - }; - } - - - /* - * Creates a publishing trigger to ensure Supplier does not begin publishing until binding is created - */ - private Publisher setupBindingTrigger(GenericApplicationContext context) { - AtomicReference> triggerRef = new AtomicReference<>(); - Publisher beginPublishingTrigger = Mono.create(emmiter -> { - triggerRef.set(emmiter); - }); - context.addApplicationListener(event -> { - if (event instanceof BindingCreatedEvent) { - if (triggerRef.get() != null) { - triggerRef.get().success(); - } - } - }); - return beginPublishingTrigger; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private IntegrationFlowBuilder integrationFlowFromProvidedSupplier(Supplier supplier, - Publisher beginPublishingTrigger, PollableBean pollable, GenericApplicationContext context, - TaskScheduler taskScheduler, ProducerProperties producerProperties, String bindingName) { - - IntegrationFlowBuilder integrationFlowBuilder; - - boolean splittable = pollable != null - && (boolean) AnnotationUtils.getAnnotationAttributes(pollable).get("splittable"); - - FunctionInvocationWrapper function = - (supplier instanceof PartitionAwareFunctionWrapper partitionAwareFunctionWrapper) - ? (FunctionInvocationWrapper) partitionAwareFunctionWrapper.function : (FunctionInvocationWrapper) supplier; - boolean reactive = FunctionTypeUtils.isPublisher(function.getOutputType()); - - if (pollable == null && reactive) { - Publisher publisher = (Publisher) supplier.get(); - publisher = publisher instanceof Mono - ? ((Mono) publisher).delaySubscription(beginPublishingTrigger).map(this::wrapToMessageIfNecessary) - : ((Flux) publisher).delaySubscription(beginPublishingTrigger).map(this::wrapToMessageIfNecessary); - - integrationFlowBuilder = IntegrationFlow.from(publisher); - - // see https://github.com/spring-cloud/spring-cloud-stream/issues/1863 for details about the following code - taskScheduler.schedule(() -> { }, Instant.now()); // will keep AC alive - } - else { // implies pollable - AtomicReference pollerMetadata = new AtomicReference<>(); - if (producerProperties != null && producerProperties.getPoller() != null) { - PollerProperties poller = producerProperties.getPoller(); - - PollerMetadata pm = new PollerMetadata(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(poller::getMaxMessagesPerPoll).to(pm::setMaxMessagesPerPoll); - map.from(poller).as(this::asTrigger).to(pm::setTrigger); - pollerMetadata.set(pm); - } - - boolean autoStartup = producerProperties != null ? producerProperties.isAutoStartup() : true; - integrationFlowBuilder = pollerMetadata == null - ? IntegrationFlow.fromSupplier(supplier, - spca -> spca.id(bindingName + "_spca").autoStartup(autoStartup)) - : IntegrationFlow.fromSupplier(supplier, spca -> spca.id(bindingName + "_spca") - .poller(pollerMetadata.get()).autoStartup(autoStartup)); - - // only apply the PollableBean attributes if this is a reactive function. - if (splittable && reactive) { - integrationFlowBuilder = integrationFlowBuilder.split(); - } - } - - return integrationFlowBuilder; - } - - private Trigger asTrigger(PollerProperties poller) { - if (StringUtils.hasText(poller.getCron())) { - return new CronTrigger(poller.getCron()); - } - return createPeriodicTrigger(poller.getFixedDelay(), poller.getInitialDelay()); - } - - private Trigger createPeriodicTrigger(Duration period, Duration initialDelay) { - PeriodicTrigger trigger = new PeriodicTrigger(period); - if (initialDelay != null) { - trigger.setInitialDelay(initialDelay); - } - return trigger; - } - - private PollableBean extractPollableAnnotation(StreamFunctionProperties functionProperties, GenericApplicationContext context, - BindableFunctionProxyFactory proxyFactory) { - // here we need to ensure that for cases where composition is defined we only look for supplier method to find Pollable annotation. - String supplierFunctionName = StringUtils - .delimitedListToStringArray(proxyFactory.getFunctionDefinition().replaceAll(",", "|").trim(), "|")[0]; - BeanDefinition bd = context.getBeanDefinition(supplierFunctionName); - if (!(bd instanceof RootBeanDefinition rootBeanDefinition)) { - return null; - } - - Method factoryMethod = rootBeanDefinition.getResolvedFactoryMethod(); - if (factoryMethod == null) { - Object source = bd.getSource(); - if (source instanceof MethodMetadata methodMetadata) { - Class factory = ClassUtils.resolveClassName(methodMetadata.getDeclaringClassName(), null); - Class[] params = FunctionContextUtils.getParamTypesFromBeanDefinitionFactory(factory, (RootBeanDefinition) bd); - factoryMethod = ReflectionUtils.findMethod(factory, methodMetadata.getMethodName(), params); - } - } - Assert.notNull(factoryMethod, "Failed to introspect factory method since it was not discovered for function '" - + functionProperties.getDefinition() + "'"); - return factoryMethod.getReturnType().isAssignableFrom(Supplier.class) - ? AnnotationUtils.findAnnotation(factoryMethod, PollableBean.class) - : null; - } - - - @SuppressWarnings("unchecked") - private Message wrapToMessageIfNecessary(T value) { - return value instanceof Message message - ? message - : MessageBuilder.withPayload(value).build(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static Message sanitize(Message inputMessage) { - MessageBuilder builder = MessageBuilder - .fromMessage(inputMessage) - .removeHeader("spring.cloud.stream.sendto.destination") - .removeHeader(MessageUtils.SOURCE_TYPE); - if (builder.getHeaders().containsKey(MessageUtils.TARGET_PROTOCOL)) { - builder = builder.setHeader(MessageUtils.SOURCE_TYPE, builder.getHeaders().get(MessageUtils.TARGET_PROTOCOL)); - } - builder = builder.removeHeader(MessageUtils.TARGET_PROTOCOL); - return builder.build(); - } - - private static class FunctionToDestinationBinder implements InitializingBean, ApplicationContextAware { - - protected final Log logger = LogFactory.getLog(getClass()); - - private GenericApplicationContext applicationContext; - - private BindableProxyFactory[] bindableProxyFactories; - - private final FunctionCatalog functionCatalog; - - private final StreamFunctionProperties functionProperties; - - private final BindingServiceProperties serviceProperties; - - private final StreamBridge streamBridge; - - FunctionToDestinationBinder(FunctionCatalog functionCatalog, StreamFunctionProperties functionProperties, - BindingServiceProperties serviceProperties, StreamBridge streamBridge) { - this.functionCatalog = functionCatalog; - this.functionProperties = functionProperties; - this.serviceProperties = serviceProperties; - this.streamBridge = streamBridge; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = (GenericApplicationContext) applicationContext; - } - - @Override - public void afterPropertiesSet() throws Exception { - Map beansOfType = applicationContext.getBeansOfType(BindableProxyFactory.class); - this.bindableProxyFactories = beansOfType.values().toArray(new BindableProxyFactory[0]); - for (BindableProxyFactory bindableProxyFactory : this.bindableProxyFactories) { - String functionDefinition = bindableProxyFactory instanceof BindableFunctionProxyFactory functionFactory - && functionFactory.isFunctionExist() - ? functionFactory.getFunctionDefinition() - : null; /*this.functionProperties.getDefinition();*/ - - boolean shouldNotProcess = false; - if (!(bindableProxyFactory instanceof BindableFunctionProxyFactory)) { - Set outputBindingNames = bindableProxyFactory.getOutputs(); - shouldNotProcess = !CollectionUtils.isEmpty(outputBindingNames) - && outputBindingNames.iterator().next().equals("applicationMetrics"); - } - if (StringUtils.hasText(functionDefinition) && !shouldNotProcess) { - FunctionInvocationWrapper function = functionCatalog.lookup(functionDefinition); - if (function != null && !function.isSupplier() && functionDefinition.equals(function.getFunctionDefinition())) { - this.bindFunctionToDestinations(bindableProxyFactory, functionDefinition, applicationContext.getEnvironment()); - } - } - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private void bindFunctionToDestinations(BindableProxyFactory bindableProxyFactory, String functionDefinition, ConfigurableEnvironment environment) { - this.assertBindingIsPossible(bindableProxyFactory); - - Set inputBindingNames = bindableProxyFactory.getInputs(); - Set outputBindingNames = bindableProxyFactory.getOutputs(); - - String[] outputContentTypes = outputBindingNames.stream() - .map(bindingName -> this.serviceProperties.getBindings().get(bindingName).getContentType()) - .toArray(String[]::new); - - FunctionInvocationWrapper function = this.functionCatalog.lookup(functionDefinition, outputContentTypes); - this.assertSupportedSignatures(bindableProxyFactory, function); - - - if (this.functionProperties.isComposeFrom()) { - AbstractSubscribableChannel outputChannel = this.applicationContext.getBean(outputBindingNames.iterator().next(), AbstractSubscribableChannel.class); - logger.info("Composing at the head of output destination: " + outputChannel.getBeanName()); - String outputChannelName = ((AbstractMessageChannel) outputChannel).getBeanName(); - DirectWithAttributesChannel newOutputChannel = new DirectWithAttributesChannel(); - newOutputChannel.setAttribute("type", "output"); - newOutputChannel.setComponentName("output.extended"); - this.applicationContext.registerBean("output.extended", MessageChannel.class, () -> newOutputChannel); - bindableProxyFactory.replaceOutputChannel(outputChannelName, "output.extended", newOutputChannel); - inputBindingNames = Collections.singleton("output"); - } - - if (isReactiveOrMultipleInputOutput(bindableProxyFactory, function.getInputType(), function.getOutputType())) { - AtomicReference, Message>> targetProtocolEnhancer = new AtomicReference<>(); - if (!CollectionUtils.isEmpty(outputBindingNames)) { - String outputBindingName = outputBindingNames.iterator().next(); // TODO only gets the first one - String binderConfigurationName = this.serviceProperties.getBinder(outputBindingName); - BinderFactory binderFactory = applicationContext.getBean(BinderFactory.class); - final Boolean reactive = functionProperties.getReactive().get(functionDefinition); - final boolean reactiveFn = reactive != null && reactive; - Class bindableType = MessageChannel.class; - if (reactiveFn) { - bindableType = FluxMessageChannel.class; - } - Object binder = binderFactory.getBinder(binderConfigurationName, bindableType); - String targetProtocol = binder.getClass().getSimpleName().startsWith("Rabbit") ? "amqp" : "kafka"; - Field headersField = ReflectionUtils.findField(MessageHeaders.class, "headers"); - headersField.setAccessible(true); - targetProtocolEnhancer.set(message -> { - Map headersMap = (Map) ReflectionUtils - .getField(headersField, ((Message) message).getHeaders()); - headersMap.putIfAbsent(MessageUtils.TARGET_PROTOCOL, targetProtocol); - if (CloudEventMessageUtils.isCloudEvent((message))) { - headersMap.putIfAbsent(MessageUtils.MESSAGE_TYPE, CloudEventMessageUtils.CLOUDEVENT_VALUE); - } - return message; - }); - } - - Publisher[] inputPublishers = inputBindingNames.stream().map(inputBindingName -> { - BindingProperties bindingProperties = this.serviceProperties.getBindings().get(inputBindingName); - ConsumerProperties consumerProperties = bindingProperties == null ? null : bindingProperties.getConsumer(); - if (consumerProperties != null) { - function.setSkipInputConversion(consumerProperties.isUseNativeDecoding()); - if (consumerProperties.getConcurrency() > 1) { - this.logger.warn("When using concurrency > 1 in reactive contexts, please make sure that you are using a " + - "reactive binder that supports concurrency settings. Otherwise, concurrency settings > 1 will be ignored when " + - "using reactive types."); - } - } - MessageChannel inputChannel = this.applicationContext.getBean(inputBindingName, MessageChannel.class); - return IntegrationReactiveUtils.messageChannelToFlux(inputChannel).map(m -> { - if (m instanceof Message) { - m = sanitize(m); - } - return m; - }); - }) - .map(publisher -> { - if (targetProtocolEnhancer.get() != null) { - return publisher.map(targetProtocolEnhancer.get()); - } - else { - return publisher; - } - }) - .toArray(Publisher[]::new); - - Function functionToInvoke = function; - if (!CollectionUtils.isEmpty(outputBindingNames)) { - BindingProperties bindingProperties = this.serviceProperties.getBindings().get(outputBindingNames.iterator().next()); - ProducerProperties producerProperties = bindingProperties == null ? null : bindingProperties.getProducer(); - if (producerProperties != null) { - function.setSkipOutputConversion(producerProperties.isUseNativeEncoding()); - } - functionToInvoke = new PartitionAwareFunctionWrapper(function, this.applicationContext, producerProperties); - // If we have a multi-output scenario, we will do any message enrichment (aka, determining the outbound - // partition) via the corresponding reactive Flux types. Currently, we support multiple output - // bindings for reactive types only (Tuples). - if (outputBindingNames.size() > 1) { - ((PartitionAwareFunctionWrapper) functionToInvoke).setMessageEnricherEnabled(false); - } - } - Object resultPublishers = functionToInvoke.apply(inputPublishers.length == 1 ? inputPublishers[0] : Tuples.fromArray(inputPublishers)); - if (!(resultPublishers instanceof Iterable)) { - resultPublishers = Collections.singletonList(resultPublishers); - } - Iterator outputBindingIter = outputBindingNames.iterator(); - long outputCount = StreamSupport.stream(((Iterable) resultPublishers).spliterator(), false).count(); - - ((Iterable) resultPublishers).forEach(publisher -> { - Flux flux = Flux.from((Publisher) publisher); - if (!CollectionUtils.isEmpty(outputBindingNames)) { - String outputBinding = outputBindingIter.next(); - MessageChannel outputChannel = this.applicationContext.getBean(outputBinding, MessageChannel.class); - flux = flux.doOnNext(message -> { - // If there are more than 1 output bindings, then ensure that we properly calculate the partitions - // based on information from the correct output binding properties. - if (outputCount > 1) { - Integer partitionId = determinePartitionForOutputBinding(outputBinding, message); - message = MessageBuilder - .fromMessage((Message) message) - .setHeader(BinderHeaders.PARTITION_HEADER, partitionId).build(); - } - if (message instanceof Message m && m.getHeaders().get("spring.cloud.stream.sendto.destination") != null) { - String destinationName = (String) m.getHeaders().get("spring.cloud.stream.sendto.destination"); - ProducerProperties producerProperties = this.serviceProperties.getBindings().get(outputBindingNames.iterator().next()).getProducer(); - MessageChannel dynamicChannel = streamBridge.resolveDestination(destinationName, producerProperties, null); - if (logger.isInfoEnabled()) { - logger.info("Output message is sent to '" + destinationName + "' destination"); - } - dynamicChannel.send(m); - } - else { - if (!(message instanceof Message)) { - message = MessageBuilder.withPayload(message).build(); - } - outputChannel.send((Message) message); - } - }) - .doOnError(e -> { - logger.error("Failure was detected during execution of the reactive function '" + functionDefinition + "'"); - ((Throwable) e).printStackTrace(); - }); - } - if (!function.isConsumer()) { - flux.subscribe(); - } - }); - } - else { - String outputDestinationName = this.determineOutputDestinationName(0, bindableProxyFactory, function.isConsumer()); - if (!ObjectUtils.isEmpty(inputBindingNames)) { - String inputDestinationName = inputBindingNames.iterator().next(); - Object inputDestination = this.applicationContext.getBean(inputDestinationName); - if (inputDestination != null && inputDestination instanceof SubscribableChannel) { - AbstractMessageHandler handler = createFunctionHandler(function, inputDestinationName, outputDestinationName); - ((SubscribableChannel) inputDestination).subscribe(handler); - } - } - } - } - - private Integer determinePartitionForOutputBinding(String outputBinding, Object message) { - BindingProperties bindingProperties = FunctionToDestinationBinder.this.serviceProperties.getBindings().get(outputBinding); - ProducerProperties producerProperties = bindingProperties == null ? null : bindingProperties.getProducer(); - if (producerProperties != null && producerProperties.isPartitioned()) { - StandardEvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.applicationContext.getBeanFactory()); - PartitionHandler partitionHandler = new PartitionHandler(evaluationContext, producerProperties, this.applicationContext.getBeanFactory()); - if (message instanceof Message) { - return partitionHandler.determinePartition((Message) message); - } - } - return null; - } - - private AbstractMessageHandler createFunctionHandler(FunctionInvocationWrapper function, - String inputChannelName, String outputChannelName) { - ConsumerProperties consumerProperties = StringUtils.hasText(inputChannelName) - ? this.serviceProperties.getBindingProperties(inputChannelName).getConsumer() - : null; - ProducerProperties producerProperties = StringUtils.hasText(outputChannelName) - ? this.serviceProperties.getBindingProperties(outputChannelName).getProducer() - : null; - - FunctionWrapper functionInvocationWrapper = (new FunctionWrapper(function, consumerProperties, - producerProperties, applicationContext, this.determineTargetProtocol(outputChannelName))); - - MessagingTemplate template = new MessagingTemplate(); - template.setBeanFactory(applicationContext.getBeanFactory()); - - AbstractMessageHandler handler = new AbstractMessageHandler() { - @SuppressWarnings("unchecked") - @Override - public void handleMessageInternal(Message message) throws MessagingException { - Object result = functionInvocationWrapper.apply((Message) message); - if (result == null) { - logger.debug("Function execution resulted in null. No message will be sent"); - return; - } - if (result instanceof Iterable iterableResult) { - for (Object resultElement : iterableResult) { - this.doSendMessage(resultElement, message); - } - } - else if (ObjectUtils.isArray(result) && !(result instanceof byte[])) { - for (int i = 0; i < ((Object[]) result).length; i++) { - this.doSendMessage(((Object[]) result)[i], message); - } - } - else { - this.doSendMessage(result, message); - } - } - - private void doSendMessage(Object result, Message requestMessage) { - if (result instanceof Message messageResult && messageResult.getHeaders().get("spring.cloud.stream.sendto.destination") != null) { - String destinationName = (String) messageResult.getHeaders().get("spring.cloud.stream.sendto.destination"); - MessageChannel outputChannel = streamBridge.resolveDestination(destinationName, producerProperties, null); - BindingProperties bindingProperties = serviceProperties.getBindingProperties(destinationName); - ProducerProperties sendToBindingProducerProperties = bindingProperties.getProducer(); - if (sendToBindingProducerProperties != null && sendToBindingProducerProperties.isPartitioned()) { - ((AbstractMessageChannel) outputChannel) - .addInterceptor(new DefaultPartitioningInterceptor(bindingProperties, applicationContext.getBeanFactory())); - } - if (logger.isInfoEnabled()) { - logger.info("Output message is sent to '" + destinationName + "' destination"); - } - outputChannel.send(messageResult); - } - else if (StringUtils.hasText(outputChannelName)) { - if (!(result instanceof Message)) { - result = MessageBuilder.withPayload(result).copyHeadersIfAbsent(requestMessage.getHeaders()).build(); - } - template.send(outputChannelName, (Message) result); - } - else if (function.isRoutingFunction()) { - if (!(result instanceof Message)) { - result = MessageBuilder.withPayload(result).copyHeadersIfAbsent(requestMessage.getHeaders()).build(); - } - streamBridge.send(function.getFunctionDefinition() + "-out-0", result); - } - function.postProcess(); - } - - }; - - handler.setBeanFactory(this.applicationContext); - handler.afterPropertiesSet(); - return handler; - } - - private String determineTargetProtocol(String outputBindingName) { - if (StringUtils.hasText(outputBindingName)) { - String binderConfigurationName = this.serviceProperties.getBinder(outputBindingName); - BinderFactory binderFactory = applicationContext.getBean(BinderFactory.class); - Object binder = binderFactory.getBinder(binderConfigurationName, MessageChannel.class); - String protocol = binder.getClass().getSimpleName().startsWith("Rabbit") ? "amqp" : "kafka"; - return protocol; - } - return null; - } - - private boolean isReactiveOrMultipleInputOutput(BindableProxyFactory bindableProxyFactory, Type inputType, Type outputType) { - boolean reactiveInputsOutputs = FunctionTypeUtils.isPublisher(inputType) || - FunctionTypeUtils.isPublisher(outputType); - return isMultipleInputOutput(bindableProxyFactory) || reactiveInputsOutputs; - } - - private String determineOutputDestinationName(int index, BindableProxyFactory bindableProxyFactory, boolean isConsumer) { - List outputNames = new ArrayList<>(bindableProxyFactory.getOutputs()); - if (CollectionUtils.isEmpty(outputNames)) { - outputNames = Collections.singletonList("output"); - } - String outputDestinationName = bindableProxyFactory instanceof BindableFunctionProxyFactory - ? ((BindableFunctionProxyFactory) bindableProxyFactory).getOutputName(index) - : (isConsumer ? null : outputNames.get(index)); - return outputDestinationName; - } - - private void assertBindingIsPossible(BindableProxyFactory bindableProxyFactory) { - if (this.isMultipleInputOutput(bindableProxyFactory)) { - Assert.isTrue(!functionProperties.isComposeTo() && !functionProperties.isComposeFrom(), - "Composing to/from existing Sinks and Sources are not supported for functions with multiple arguments."); - } - } - - private boolean isMultipleInputOutput(BindableProxyFactory bindableProxyFactory) { - return bindableProxyFactory instanceof BindableFunctionProxyFactory - && ((BindableFunctionProxyFactory) bindableProxyFactory).isMultiple(); - } - - private boolean isArray(Type type) { - return type instanceof GenericArrayType || type instanceof Class && ((Class) type).isArray(); - } - - private void assertSupportedSignatures(BindableProxyFactory bindableProxyFactory, FunctionInvocationWrapper function) { - if (this.isMultipleInputOutput(bindableProxyFactory)) { - Assert.isTrue(!function.isConsumer(), - "Function '" + functionProperties.getDefinition() + "' is a Consumer which is not supported " - + "for multi-in/out reactive streams. Only Functions are supported"); - Assert.isTrue(!function.isSupplier(), - "Function '" + functionProperties.getDefinition() + "' is a Supplier which is not supported " - + "for multi-in/out reactive streams. Only Functions are supported"); - Assert.isTrue(!this.isArray(function.getInputType()) && !this.isArray(function.getOutputType()), - "Function '" + functionProperties.getDefinition() + "' has the following signature: [" - + function + "]. Your input and/or outout lacks arity and therefore we " - + "can not determine how many input/output destinations are required in the context of " - + "function input/output binding."); - } - } - - } - - /** - * - * It's signatures ensures that within the context of s-c-stream Spring Integration does - * not attempt any conversion and sends a raw Message. - */ - @SuppressWarnings("rawtypes") - private static class FunctionWrapper implements Function, Object> { - private final Function function; - - private final ConsumerProperties consumerProperties; - - private final ProducerProperties producerProperties; - - private final Field headersField; - - private final ConfigurableApplicationContext applicationContext; - - private final boolean isRoutingFunction; - - private final String targetProtocol; - - FunctionWrapper(Function function, ConsumerProperties consumerProperties, - ProducerProperties producerProperties, ConfigurableApplicationContext applicationContext, String targetProtocol) { - - isRoutingFunction = ((FunctionInvocationWrapper) function).getTarget() instanceof RoutingFunction; - this.applicationContext = applicationContext; - this.function = new PartitionAwareFunctionWrapper(function, this.applicationContext, producerProperties); - this.consumerProperties = consumerProperties; - if (this.consumerProperties != null) { - ((FunctionInvocationWrapper) function).setSkipInputConversion(this.consumerProperties.isUseNativeDecoding()); - } - this.producerProperties = producerProperties; - if (this.producerProperties != null) { - ((FunctionInvocationWrapper) function).setSkipOutputConversion(this.producerProperties.isUseNativeEncoding()); - } - this.headersField = ReflectionUtils.findField(MessageHeaders.class, "headers"); - this.headersField.setAccessible(true); - this.targetProtocol = targetProtocol; - } - - @SuppressWarnings("unchecked") - @Override - public Object apply(Message message) { - message = sanitize(message); - Map headersMap = (Map) ReflectionUtils - .getField(this.headersField, message.getHeaders()); - if (StringUtils.hasText(targetProtocol)) { - headersMap.putIfAbsent(MessageUtils.TARGET_PROTOCOL, targetProtocol); - } - if (CloudEventMessageUtils.isCloudEvent(message)) { - headersMap.putIfAbsent(MessageUtils.MESSAGE_TYPE, CloudEventMessageUtils.CLOUDEVENT_VALUE); - } - Object result = function.apply(message); - if (result instanceof Publisher && this.isRoutingFunction) { - throw new IllegalStateException("Routing to functions that return Publisher " - + "is not supported in the context of Spring Cloud Stream."); - } - return result; - } - } - - /** - * Creates and registers instances of BindableFunctionProxyFactory for each user defined function - * thus triggering destination bindings between function arguments and destinations. - * - * In other words this class is responsible to do the same work as EnableBinding except that it derives the input/output names - * from the names of the function (e.g., function-in-0). - */ - private static class FunctionBindingRegistrar implements InitializingBean, ApplicationContextAware, EnvironmentAware { - - protected final Log logger = LogFactory.getLog(getClass()); - - private final FunctionCatalog functionCatalog; - - private final StreamFunctionProperties streamFunctionProperties; - - private ConfigurableApplicationContext applicationContext; - - private Environment environment; - - private int inputCount; - - private int outputCount; - - FunctionBindingRegistrar(FunctionCatalog functionCatalog, StreamFunctionProperties streamFunctionProperties) { - this.functionCatalog = functionCatalog; - this.streamFunctionProperties = streamFunctionProperties; - } - - @Override - public void afterPropertiesSet() throws Exception { - this.determineFunctionName(functionCatalog, environment); - - if (StringUtils.hasText(streamFunctionProperties.getDefinition())) { - String[] functionDefinitions = this.filterEligibleFunctionDefinitions(); - for (String functionDefinition : functionDefinitions) { - - FunctionInvocationWrapper function = functionCatalog.lookup(functionDefinition); - if (function != null) { - if (function.isSupplier()) { - this.inputCount = 0; - this.outputCount = this.getOutputCount(function, true); - } - else if (function.isConsumer() || function.isRoutingFunction()) { - this.inputCount = FunctionTypeUtils.getInputCount(function); - this.outputCount = 0; - } - else { - this.inputCount = FunctionTypeUtils.getInputCount(function); - if (function.isWrappedBiConsumer()) { - this.outputCount = 0; - } - else { - this.outputCount = this.getOutputCount(function, false); - } - } - - AtomicReference proxyFactory = new AtomicReference<>(); - if (function.isInputTypePublisher()) { - final SupportedBindableFeatures supportedBindableFeatures = new SupportedBindableFeatures(); - supportedBindableFeatures.setPollable(false); - supportedBindableFeatures.setReactive(true); - - proxyFactory.set(new BindableFunctionProxyFactory(functionDefinition, - this.inputCount, this.outputCount, this.streamFunctionProperties, supportedBindableFeatures)); - } - else { - proxyFactory.set(new BindableFunctionProxyFactory(functionDefinition, - this.inputCount, this.outputCount, this.streamFunctionProperties)); - } - ((GenericApplicationContext) this.applicationContext).registerBean(functionDefinition + "_binding", - BindableFunctionProxyFactory.class, () -> proxyFactory.get()); - } - else { - logger.warn("The function definition '" + streamFunctionProperties.getDefinition() + - "' is not valid. The referenced function bean or one of its components does not exist"); - } - } - } - - this.createStandAloneBindingsIfNecessary(applicationContext.getBean(BindingServiceProperties.class)); - - } - - private void createStandAloneBindingsIfNecessary(BindingServiceProperties bindingProperties) { - String[] inputBindings = StringUtils.hasText(bindingProperties.getInputBindings()) - ? bindingProperties.getInputBindings().split(";") : new String[0]; - - String[] outputBindings = StringUtils.hasText(bindingProperties.getOutputBindings()) ? bindingProperties.getOutputBindings().split(";") : ( - StringUtils.hasText(bindingProperties.getOutputBindings()) ? bindingProperties.getOutputBindings().split(";") : new String[0] - ); - - for (String inputBindingName : inputBindings) { - FunctionInvocationWrapper sourceFunc = functionCatalog.lookup(inputBindingName); - if (sourceFunc != null && !sourceFunc.getFunctionDefinition().equals(inputBindingName)) { - sourceFunc = null; - } - - if (sourceFunc == null || //see https://github.com/spring-cloud/spring-cloud-stream/issues/2229 - sourceFunc.isSupplier() || - (!sourceFunc.getFunctionDefinition().equals(inputBindingName) && applicationContext.containsBean(inputBindingName))) { - - BindableFunctionProxyFactory proxyFactory = new BindableFunctionProxyFactory(inputBindingName, 1, 0, this.streamFunctionProperties, sourceFunc != null); - ((GenericApplicationContext) this.applicationContext).registerBean(inputBindingName + "_binding_in", - BindableFunctionProxyFactory.class, () -> proxyFactory); - } - } - - for (String outputBindingName : outputBindings) { - FunctionInvocationWrapper sourceFunc = functionCatalog.lookup(outputBindingName); - if (sourceFunc != null && !sourceFunc.getFunctionDefinition().equals(outputBindingName)) { - sourceFunc = null; - } - - if (sourceFunc == null || //see https://github.com/spring-cloud/spring-cloud-stream/issues/2229 - sourceFunc.isConsumer() || - (!sourceFunc.getFunctionDefinition().equals(outputBindingName) && applicationContext.containsBean(outputBindingName))) { - - BindableFunctionProxyFactory proxyFactory = new BindableFunctionProxyFactory(outputBindingName, 0, 1, this.streamFunctionProperties, sourceFunc != null); - ((GenericApplicationContext) this.applicationContext).registerBean(outputBindingName + "_binding_out", - BindableFunctionProxyFactory.class, () -> proxyFactory); - } - } - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; - } - - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - private int getOutputCount(FunctionInvocationWrapper function, boolean isSupplier) { - int outputCount = FunctionTypeUtils.getOutputCount(function); - if (!function.isSupplier() && function.getOutputType() instanceof ParameterizedType) { - Type outputType = function.getOutputType(); - if (FunctionTypeUtils.isMono(outputType) && outputType instanceof ParameterizedType - && FunctionTypeUtils.getRawType(((ParameterizedType) outputType).getActualTypeArguments()[0]).equals(Void.class)) { - outputCount = 0; - } - else if (FunctionTypeUtils.getRawType(outputType).equals(Void.class)) { - outputCount = 0; - } - } - return outputCount; - } - - private boolean determineFunctionName(FunctionCatalog catalog, Environment environment) { - boolean autodetect = environment.getProperty("spring.cloud.stream.function.autodetect", boolean.class, true); - String definition = streamFunctionProperties.getDefinition(); - if (!StringUtils.hasText(definition)) { - definition = environment.getProperty("spring.cloud.function.definition"); - } - - if (StringUtils.hasText(definition)) { - streamFunctionProperties.setDefinition(definition); - } - else if (Boolean.parseBoolean(environment.getProperty("spring.cloud.stream.function.routing.enabled", "false")) - || environment.containsProperty("spring.cloud.function.routing-expression")) { - streamFunctionProperties.setDefinition(RoutingFunction.FUNCTION_NAME); - } - else if (autodetect) { - FunctionInvocationWrapper function = functionCatalog.lookup(""); - if (function != null) { - streamFunctionProperties.setDefinition(function.getFunctionDefinition()); - } - } - return StringUtils.hasText(streamFunctionProperties.getDefinition()); - } - - /* - * This is to accommodate Kafka streams binder, since it does not rely on binding mechanism provided by s-c-stream core. - * So we basically filter out any function name who's type contains KTable or KStream. - */ - private String[] filterEligibleFunctionDefinitions() { - List eligibleFunctionDefinitions = new ArrayList<>(); - String[] functionDefinitions = streamFunctionProperties.getDefinition().split(";"); - for (String functionDefinition : functionDefinitions) { - functionDefinition = functionDefinition.trim(); - String[] functionNames = StringUtils.delimitedListToStringArray(functionDefinition.replaceAll(",", "|").trim(), "|"); - boolean eligibleDefinition = true; - for (int i = 0; i < functionNames.length && eligibleDefinition; i++) { - String functionName = functionNames[i]; - if (this.applicationContext.containsBean(functionName)) { - Object functionBean = this.applicationContext.getBean(functionName); - Type functionType = FunctionTypeUtils.discoverFunctionType(functionBean, functionName, (GenericApplicationContext) this.applicationContext); - String functionTypeStringValue = functionType.toString(); - if (functionTypeStringValue.contains("KTable") || functionTypeStringValue.contains("KStream")) { - eligibleDefinition = false; - } - - } - else { - logger.warn("You have defined function definition that does not exist: " + functionName); - } - } - if (eligibleDefinition) { - eligibleFunctionDefinitions.add(functionDefinition); - } - } - return eligibleFunctionDefinitions.toArray(new String[0]); - } - - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConstants.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConstants.java deleted file mode 100644 index 74180db3b..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/FunctionConstants.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function; - -/** - * Interfaces which defines constants used for naming conventions used in bindings - * of multiple functions or functions with multiple inputs and outputs. - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * - * @since 3.0 - * - */ -public final class FunctionConstants { - - private FunctionConstants() { - - } - - /** - * Delimiter used for functional binding naming convention. - */ - public static final String DELIMITER = "-"; - - /** - * Suffix used for functional binding naming convention. - */ - public static final String DEFAULT_OUTPUT_SUFFIX = "out"; - - /** - * Prefix used for functional binding naming convention. - */ - public static final String DEFAULT_INPUT_SUFFIX = "in"; -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/PartitionAwareFunctionWrapper.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/PartitionAwareFunctionWrapper.java deleted file mode 100644 index 882040ff9..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/PartitionAwareFunctionWrapper.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.util.function.Function; -import java.util.function.Supplier; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.stream.binder.BinderHeaders; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.integration.expression.ExpressionUtils; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.util.ObjectUtils; - -/** - * This class is effectively a wrapper which is aware of the stream related partition information - * for outgoing messages. It has only one responsibility and that is to modify the result message - * with 'scst_partition' header if necessary. - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * @author Byungjun You - */ -class PartitionAwareFunctionWrapper implements Function, Supplier { - - protected final Log logger = LogFactory.getLog(PartitionAwareFunctionWrapper.class); - - @SuppressWarnings("rawtypes") - protected final Function function; - - private final Function outputMessageEnricher; - - private boolean messageEnricherEnabled = true; - - PartitionAwareFunctionWrapper(Function function, ConfigurableApplicationContext context, ProducerProperties producerProperties) { - this.function = function; - - if (producerProperties != null && producerProperties.isPartitioned()) { - StandardEvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(context.getBeanFactory()); - PartitionHandler partitionHandler = new PartitionHandler(evaluationContext, producerProperties, context.getBeanFactory()); - - this.outputMessageEnricher = output -> { - if ((ObjectUtils.isArray(output) && !(output instanceof byte[])) || output instanceof Iterable) { - return output; - } - else if (!(output instanceof Message)) { - output = MessageBuilder.withPayload(output).build(); - } - return toMessageWithPartitionHeader((Message) output, partitionHandler); - }; - } - else { - this.outputMessageEnricher = null; - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Message toMessageWithPartitionHeader(Message message, PartitionHandler partitionHandler) { - int partitionId = partitionHandler.determinePartition(message); - return MessageBuilder - .fromMessage(message) - .setHeader(BinderHeaders.PARTITION_HEADER, partitionId).build(); - } - - @SuppressWarnings("unchecked") - @Override - public Object apply(Object input) { - if (this.messageEnricherEnabled) { - this.setEnhancerIfNecessary(); - } - Object result = this.function.apply(input); - if (!((FunctionInvocationWrapper) this.function).isInputTypePublisher()) { - ((FunctionInvocationWrapper) this.function).setEnhancer(null); - } - return result; - } - - @Override - public Object get() { - if (this.function instanceof FunctionInvocationWrapper functionInvocationWrapper) { - if (this.messageEnricherEnabled) { - this.setEnhancerIfNecessary(); - } - return functionInvocationWrapper.get(); - } - throw new IllegalStateException("Call to get() is not allowed since this function is not a Supplier."); - } - - private void setEnhancerIfNecessary() { - if (this.function instanceof FunctionInvocationWrapper functionInvocationWrapper) { - functionInvocationWrapper.setEnhancer(this.outputMessageEnricher); - } - } - - public void setMessageEnricherEnabled(boolean messageEnricherEnabled) { - this.messageEnricherEnabled = messageEnricherEnabled; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/PollableSourceInitializer.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/PollableSourceInitializer.java deleted file mode 100644 index 7bda0311c..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/PollableSourceInitializer.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import org.springframework.cloud.stream.binding.SupportedBindableFeatures; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; - -/** - * - * @author Oleg Zhurakousky - * @since 4.0.1 - * - */ -public class PollableSourceInitializer implements ApplicationContextInitializer { - - @Override - public void initialize(GenericApplicationContext applicationContext) { - Environment environment = applicationContext.getEnvironment(); - if (StringUtils.hasText(environment.getProperty("spring.cloud.stream.pollable-source"))) { - String[] sourceNames = environment.getProperty("spring.cloud.stream.pollable-source").split(";"); - - for (String sourceName : sourceNames) { - final SupportedBindableFeatures supportedBindableFeatures = new SupportedBindableFeatures(); - supportedBindableFeatures.setPollable(true); - supportedBindableFeatures.setReactive(false); - - BindableFunctionProxyFactory proxyFactory = - new BindableFunctionProxyFactory(sourceName, 1, 0, new StreamFunctionProperties(), supportedBindableFeatures); - applicationContext.registerBean(sourceName + "_binding", BindableFunctionProxyFactory.class, () -> proxyFactory); - } - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/RoutingFunctionEnvironmentPostProcessor.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/RoutingFunctionEnvironmentPostProcessor.java deleted file mode 100644 index 4ce172626..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/RoutingFunctionEnvironmentPostProcessor.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.function; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.cloud.function.context.config.RoutingFunction; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.util.StringUtils; -/** - * - * @author Oleg Zhurakousky - * @since 2.2.1 - */ -class RoutingFunctionEnvironmentPostProcessor implements EnvironmentPostProcessor { - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - String name = environment.getProperty("spring.cloud.function.definition"); - if (StringUtils.hasText(name) && ( - name.equals(RoutingFunction.FUNCTION_NAME) || - name.contains(RoutingFunction.FUNCTION_NAME + "|") || - name.contains("|" + RoutingFunction.FUNCTION_NAME) - )) { - ((StandardEnvironment) environment).getSystemProperties() - .putIfAbsent("spring.cloud.function.routing.enabled", "true"); - } - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamBridge.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamBridge.java deleted file mode 100644 index 8123f8a7f..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamBridge.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2020-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.lang.reflect.Type; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.Function; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.FunctionRegistry; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.PassThruFunction; -import org.springframework.cloud.function.context.message.MessageUtils; -import org.springframework.cloud.function.core.FunctionInvocationHelper; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.ProducerProperties; -import org.springframework.cloud.stream.binding.BindingService; -import org.springframework.cloud.stream.binding.NewDestinationBindingCallback; -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.integration.channel.AbstractMessageChannel; -import org.springframework.integration.config.GlobalChannelInterceptorProcessor; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.util.Assert; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * A class which allows user to send data to an output binding. - * While in a common scenario of a typical spring-cloud-stream application user rarely - * has to manually send data, there are times when the sources of data are outside of - * spring-cloud-stream context and therefore we need to bridge such foreign sources - * with spring-cloud-stream. - *

- * This utility class allows user to do just that - bridge non-spring-cloud-stream applications - * with spring-cloud-stream by providing a mechanism (bridge) to send data to an output binding while - * maintaining the same invocation contract (i.e., type conversion, partitioning etc) as if it was - * done through a declared function. - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * @author Byungjun You - * @since 3.0.3 - * - */ -@SuppressWarnings("rawtypes") -public final class StreamBridge implements StreamOperations, SmartInitializingSingleton { - - private static final String STREAM_BRIDGE_FUNC_NAME = "streamBridge"; - - private final Log logger = LogFactory.getLog(getClass()); - - private final Map channelCache; - - private final FunctionCatalog functionCatalog; - - private final NewDestinationBindingCallback destinationBindingCallback; - - private final BindingServiceProperties bindingServiceProperties; - - private final ConfigurableApplicationContext applicationContext; - - private boolean initialized; - - private final BindingService bindingService; - - private final Map streamBridgeFunctionCache; - - private final FunctionInvocationHelper functionInvocationHelper; - - /** - * - * @param functionCatalog instance of {@link FunctionCatalog} - * @param bindingServiceProperties instance of {@link BindingServiceProperties} - * @param applicationContext instance of {@link ConfigurableApplicationContext} - */ - @SuppressWarnings("serial") - StreamBridge(FunctionCatalog functionCatalog, BindingServiceProperties bindingServiceProperties, - ConfigurableApplicationContext applicationContext, @Nullable NewDestinationBindingCallback destinationBindingCallback) { - Assert.notNull(functionCatalog, "'functionCatalog' must not be null"); - Assert.notNull(applicationContext, "'applicationContext' must not be null"); - Assert.notNull(bindingServiceProperties, "'bindingServiceProperties' must not be null"); - this.bindingService = applicationContext.getBean(BindingService.class); - this.functionCatalog = functionCatalog; - this.applicationContext = applicationContext; - this.bindingServiceProperties = bindingServiceProperties; - this.destinationBindingCallback = destinationBindingCallback; - this.channelCache = new LinkedHashMap() { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - boolean remove = size() > bindingServiceProperties.getDynamicDestinationCacheSize(); - if (remove) { - if (logger.isDebugEnabled()) { - logger.debug("Removing message channel from cache " + eldest.getKey()); - } - bindingService.unbindProducers(eldest.getKey()); - } - return remove; - } - }; - this.functionInvocationHelper = applicationContext.getBean(FunctionInvocationHelper.class); - this.streamBridgeFunctionCache = new HashMap<>(); - } - - @Override - public boolean send(String bindingName, Object data) { - BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(bindingName); - MimeType contentType = StringUtils.hasText(bindingProperties.getContentType()) ? MimeType.valueOf(bindingProperties.getContentType()) : MimeTypeUtils.APPLICATION_JSON; - return this.send(bindingName, data, contentType); - } - - @Override - public boolean send(String bindingName, Object data, MimeType outputContentType) { - return this.send(bindingName, null, data, outputContentType); - } - @Override - public boolean send(String bindingName, @Nullable String binderName, Object data) { - return this.send(bindingName, binderName, data, MimeTypeUtils.APPLICATION_JSON); - } - - @Override - @SuppressWarnings({ "unchecked"}) - public boolean send(String bindingName, @Nullable String binderName, Object data, MimeType outputContentType) { - if (!this.initialized) { - this.afterSingletonsInstantiated(); - } - ProducerProperties producerProperties = this.bindingServiceProperties.getProducerProperties(bindingName); - MessageChannel messageChannel = this.resolveDestination(bindingName, producerProperties, binderName); - - Function functionToInvoke = this.getStreamBridgeFunction(outputContentType.toString(), producerProperties); - - if (producerProperties != null && producerProperties.isPartitioned()) { - functionToInvoke = new PartitionAwareFunctionWrapper(functionToInvoke, this.applicationContext, producerProperties); - } - - String targetType = this.resolveBinderTargetType(bindingName, binderName, MessageChannel.class, - this.applicationContext.getBean(BinderFactory.class)); - - Message messageToSend = data instanceof Message messageData - ? MessageBuilder.fromMessage(messageData).setHeaderIfAbsent(MessageUtils.TARGET_PROTOCOL, targetType).build() - : new GenericMessage<>(data, Collections.singletonMap(MessageUtils.TARGET_PROTOCOL, targetType)); - - Message resultMessage; - synchronized (this) { - resultMessage = (Message) functionToInvoke.apply(messageToSend); - } - - if (resultMessage == null - && ((Message) messageToSend).getPayload().getClass().getName().equals("org.springframework.kafka.support.KafkaNull")) { - resultMessage = messageToSend; - } - - resultMessage = (Message) this.functionInvocationHelper.postProcessResult(resultMessage, null); - - return messageChannel.send(resultMessage); - } - - private int hashProducerProperties(ProducerProperties producerProperties, String outputContentType) { - int hash = outputContentType.hashCode() - + Boolean.hashCode(producerProperties.isUseNativeEncoding()) - + Boolean.hashCode(producerProperties.isPartitioned()) - + producerProperties.getPartitionCount(); - - return hash; - } - - private synchronized FunctionInvocationWrapper getStreamBridgeFunction(String outputContentType, ProducerProperties producerProperties) { - int streamBridgeFunctionKey = this.hashProducerProperties(producerProperties, outputContentType); - if (this.streamBridgeFunctionCache.containsKey(streamBridgeFunctionKey)) { - return this.streamBridgeFunctionCache.get(streamBridgeFunctionKey); - } - else { - FunctionInvocationWrapper functionToInvoke = this.functionCatalog.lookup(STREAM_BRIDGE_FUNC_NAME, outputContentType.toString()); - this.streamBridgeFunctionCache.put(streamBridgeFunctionKey, functionToInvoke); - functionToInvoke.setSkipOutputConversion(producerProperties.isUseNativeEncoding()); - return functionToInvoke; - } - } - - @Override - public void afterSingletonsInstantiated() { - if (this.initialized) { - return; - } - - FunctionRegistration> fr = new FunctionRegistration<>(new PassThruFunction(), STREAM_BRIDGE_FUNC_NAME); - fr.getProperties().put("singleton", "false"); - - Type functionType = ResolvableType.forClassWithGenerics(Function.class, Object.class, Object.class).getType(); - ((FunctionRegistry) this.functionCatalog).register(fr.type(functionType)); - this.initialized = true; - } - - @SuppressWarnings({ "unchecked"}) - synchronized MessageChannel resolveDestination(String destinationName, ProducerProperties producerProperties, String binderName) { - MessageChannel messageChannel = null; - if (StringUtils.hasText(binderName)) { - messageChannel = this.channelCache.get(binderName + ":" + destinationName); - } - else { - messageChannel = this.channelCache.get(destinationName); - } - if (messageChannel == null) { - if (this.applicationContext.containsBean(destinationName)) { - messageChannel = this.applicationContext.getBean(destinationName, MessageChannel.class); - String[] consumerBindingNames = this.bindingService.getConsumerBindingNames(); - if (ObjectUtils.containsElement(consumerBindingNames, destinationName)) { //GH-2563 - logger.warn("You seem to be sending data to the input binding. It is not " - + "recommended, since you are bypassing the binder and this the messaging system exposed by the binder."); - } - } - else { - messageChannel = new DirectWithAttributesChannel(); - if (this.destinationBindingCallback != null) { - Object extendedProducerProperties = this.bindingService - .getExtendedProducerProperties(messageChannel, destinationName); - this.destinationBindingCallback.configure(destinationName, messageChannel, - producerProperties, extendedProducerProperties); - } - - Binder binder = null; - if (StringUtils.hasText(binderName)) { - BinderFactory binderFactory = this.applicationContext.getBean(BinderFactory.class); - binder = binderFactory.getBinder(binderName, messageChannel.getClass()); - } - // Commenting out the following block due to this issue: https://github.com/spring-cloud/spring-cloud-stream/issues/2759 - // Once we confirm that there is no unknown regression, we will remove this block from here completely, - // since we already perform the partition finding algorithm once via StreamBridge#send. -// if (producerProperties != null && producerProperties.isPartitioned()) { -// BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(destinationName); -// ((AbstractMessageChannel) messageChannel) -// .addInterceptor(new DefaultPartitioningInterceptor(bindingProperties, this.applicationContext.getBeanFactory())); -// } - this.addInterceptors((AbstractMessageChannel) messageChannel, destinationName); - - this.bindingService.bindProducer(messageChannel, destinationName, true, binder); - if (StringUtils.hasText(binderName)) { - this.channelCache.put(binderName + ":" + destinationName, messageChannel); - } - else { - this.channelCache.put(destinationName, messageChannel); - } - } - } - - return messageChannel; - } - - private String resolveBinderTargetType(String channelName, String binderName, Class bindableType, BinderFactory binderFactory) { - String binderConfigurationName = binderName != null ? binderName : this.bindingServiceProperties - .getBinder(channelName); - Binder binder = binderFactory.getBinder(binderConfigurationName, bindableType); - String targetProtocol = binder.getClass().getSimpleName().startsWith("Rabbit") ? "amqp" : "kafka"; - return targetProtocol; - } - - private void addInterceptors(AbstractMessageChannel messageChannel, String destinationName) { - final GlobalChannelInterceptorProcessor globalChannelInterceptorProcessor = - this.applicationContext.getBean(GlobalChannelInterceptorProcessor.class); - globalChannelInterceptorProcessor.postProcessAfterInitialization(messageChannel, destinationName); - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamFunctionConfigurationProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamFunctionConfigurationProperties.java deleted file mode 100644 index 9c63a2d79..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamFunctionConfigurationProperties.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * @author Oleg Zhurakousky - * @author Soby Chacko - * @since 4.0.2 - */ -@ConfigurationProperties("spring.cloud.stream.function") -public class StreamFunctionConfigurationProperties { - private Map bindings = new HashMap<>(); - - public Map getBindings() { - return this.bindings; - } - - public void setBindings(Map bindings) { - this.bindings = bindings; - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamFunctionProperties.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamFunctionProperties.java deleted file mode 100644 index 866327fd4..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamFunctionProperties.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.function; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.util.StringUtils; - -/** - * @author Oleg Zhurakousky - * @author Tolga Kavukcu - * @author Soby Chacko - * @since 2.1 - */ -public class StreamFunctionProperties { - - /** - * Definition of functions to bind. If several functions need to be composed into one, - * use pipes (e.g., 'fooFunc|barFunc') - */ - private String definition; - - private BindingServiceProperties bindingServiceProperties; - - private Map bindings = new HashMap<>(); - - private boolean batchMode; - - private boolean composeTo; - - private boolean composeFrom; - - private Map reactive = new HashMap<>(); - - boolean isComposeTo() { - return composeTo; - } - - boolean isComposeFrom() { - return composeFrom; - } - - public String getDefinition() { - return this.definition; - } - - public List getOutputBindings(String functionName) { - return this.filterBindings(functionName, "-out-"); - } - - public List getInputBindings(String functionName) { - return this.filterBindings(functionName, "-in-"); - } - - void setDefinition(String definition) { - if (StringUtils.hasText(definition)) { - this.composeFrom = definition.startsWith("|"); - this.composeTo = definition.endsWith("|"); - this.definition = this.composeFrom ? definition.substring(1) - : (this.composeTo ? definition.substring(0, definition.length() - 1) : definition); - } - } - - BindingServiceProperties getBindingServiceProperties() { - return this.bindingServiceProperties; - } - - void setBindingServiceProperties(BindingServiceProperties bindingServiceProperties) { - this.bindingServiceProperties = bindingServiceProperties; - } - - public Map getBindings() { - return this.bindings; - } - - public void setBindings(Map bindings) { - this.bindings = bindings; - } - - boolean isBatchMode() { - return this.batchMode; - } - - void setBatchMode(boolean batchMode) { - this.batchMode = batchMode; - } - - private List filterBindings(String functionName, String suffix) { - List list = bindings.keySet().stream() - .filter(bKey -> bKey.contains(functionName + suffix)) - .sorted() - .map(bKey -> bindings.get(bKey)) - .collect(Collectors.toList()); - return list; - } - - Map getReactive() { - return this.reactive; - } - - void setReactive(Map reactive) { - this.reactive = reactive; - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamOperations.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamOperations.java deleted file mode 100644 index 1cf7843a9..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/function/StreamOperations.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2022-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.cloud.stream.function; - -import org.springframework.lang.Nullable; -import org.springframework.util.MimeType; - -/** - * Basic contract for {@link StreamBridge} operations. - * - * @author Oleg Zhurakousky - * @author Soby Chacko - * - * @since 4.0.0 - */ -public interface StreamOperations { - - /** - * Sends 'data' to an output binding specified by 'bindingName' argument while - * using default content type to deal with output type conversion (if necessary). - * For typical cases `bindingName` is configured using 'spring.cloud.stream.source' property. - * However, this operation also supports sending to truly dynamic destinations. This means if the name - * provided via 'bindingName' does not have a corresponding binding such name will be - * treated as dynamic destination.
- * Will use default binder. For specific binder type see {@link #send(String, String, Object)} and - * {@link #send(String, String, Object, MimeType)} methods. - * - * @param bindingName the name of the output binding. That said it requires a bit of clarification. - * When using send("foo"...), the 'foo' typically represents the binding name. - * However, if such binding does not exist, the new binding will be created - * to support dynamic destinations. - * @param data the data to send - * @return true if data was sent successfully, otherwise false or throws an exception. - */ - boolean send(String bindingName, Object data); - - /** - * Sends 'data' to an output binding specified by 'bindingName' argument while - * using the content type specified by the 'outputContentType' argument to deal - * with output type conversion (if necessary). - * For typical cases `bindingName` is configured using 'spring.cloud.stream.source' property. - * However, this operation also supports sending to truly dynamic destinations. This means if the name - * provided via 'bindingName' does not have a corresponding binding such name will be - * treated as dynamic destination.
- * Will use default binder. For specific binder type see {@link #send(String, String, Object)} and - * {@link #send(String, String, Object, MimeType)} methods. - * - * @param bindingName the name of the output binding. That said it requires a bit of clarification. - * When using bridge.send("foo"...), the 'foo' typically represents the binding name. - * However, if such binding does not exist, the new binding will be created to support - * dynamic destinations. - * @param data the data to send - * @param outputContentType content type to be used to deal with output type conversion - * @return true if data was sent successfully, otherwise false or throws an exception. - */ - boolean send(String bindingName, Object data, MimeType outputContentType); - - /** - * Sends 'data' to an output binding specified by 'bindingName' argument while - * using the content type specified by the 'outputContentType' argument to deal - * with output type conversion (if necessary). - * For typical cases `bindingName` is configured using 'spring.cloud.stream.source' property. - * However, this operation also supports sending to truly dynamic destinations. This means if the name - * provided via 'bindingName' does not have a corresponding binding such name will be - * treated as dynamic destination. - * - * @param bindingName the name of the output binding. That said it requires a bit of clarification. - * When using bridge.send("foo"...), the 'foo' typically represents the binding name. - * However, if such binding does not exist, the new binding will be created to support dynamic destinations. - * @param binderName the name of the binder to use (e.g., 'kafka', 'rabbit') for cases where multiple - * binders are used. Can be null. - * @param data the data to send - * @return true if data was sent successfully, otherwise false or throws an exception. - */ - boolean send(String bindingName, @Nullable String binderName, Object data); - - /** - * Sends 'data' to an output binding specified by 'bindingName' argument while - * using the content type specified by the 'outputContentType' argument to deal - * with output type conversion (if necessary). - * For typical cases `bindingName` is configured using 'spring.cloud.stream.source' property. - * However, this operation also supports sending to truly dynamic destinations. This means if the name - * provided via 'bindingName' does not have a corresponding binding such name will be - * treated as dynamic destination. - * - * @param bindingName the name of the output binding. That said it requires a bit of clarification. - * When using bridge.send("foo"...), the 'foo' typically represents the binding name. - * However, if such binding does not exist, the new binding will be created to support dynamic destinations. - * @param binderName the name of the binder to use (e.g., 'kafka', 'rabbit') for cases where multiple binders are used. Can be null. - * @param data the data to send - * @param outputContentType content type to be used to deal with output type conversion - * @return true if data was sent successfully, otherwise false or throws an exception. - */ - boolean send(String bindingName, @Nullable String binderName, Object data, MimeType outputContentType); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/messaging/DirectWithAttributesChannel.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/messaging/DirectWithAttributesChannel.java deleted file mode 100644 index 71c82b5f0..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/messaging/DirectWithAttributesChannel.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.messaging; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.integration.channel.DirectChannel; -import org.springframework.messaging.MessageHandler; - -/** - * @author Oleg Zhurakousky - * @since 2.1 - */ -public class DirectWithAttributesChannel extends DirectChannel { - - private final Map attributes = new HashMap<>(); - - public void setAttribute(String key, Object value) { - this.attributes.put(key, value); - } - - public Object getAttribute(String key) { - return this.attributes.get(key); - } - - @Override - public String getBeanName() { - return this.getComponentName(); - } - - @Override - public boolean subscribe(MessageHandler handler) { - return this.getDispatcher().getHandlerCount() == 1 ? false : super.subscribe(handler); - } -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ConsumerDestination.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ConsumerDestination.java deleted file mode 100644 index 3c01c139b..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ConsumerDestination.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2015-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.provisioning; - -import org.springframework.cloud.stream.binder.ConsumerProperties; - -/** - * Represents a ConsumerDestination that provides the information about the destination - * that is physically provisioned through - * {@link ProvisioningProvider#provisionConsumerDestination(String, String, ConsumerProperties)}. - * - * @author Soby Chacko - * @since 1.2 - */ -public interface ConsumerDestination { - - /** - * Provides the destination name. - * @return destination name - */ - String getName(); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProducerDestination.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProducerDestination.java deleted file mode 100644 index bac97b9c7..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProducerDestination.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2015-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.provisioning; - -import org.springframework.cloud.stream.binder.ProducerProperties; - -/** - * Represents a ProducerDestination that provides the information about the destination - * that is physically provisioned through - * {@link ProvisioningProvider#provisionProducerDestination(String, ProducerProperties)}. - * - * @author Soby Chacko - * @since 1.2 - */ -public interface ProducerDestination { - - /** - * Provides the destination name. - * @return destination name - */ - String getName(); - - /** - * Provides the destination name for a given partition. - * - * If the producer provision the destination with partitions, on certain middleware - * brokers there may exist multiple destinations distinguishable by the partition. For - * example, if the destination name is xyz and it is provisioned with 4 - * partitions, there may be 4 different destinations on the broker such as - xyz-0, - * xyz-1, xyz-2 and xyz-3. This behavior is dependent on the broker and the way - * the corresponding binder implements the logic. - * - * On certain brokers (for instance, Kafka), this behavior is completely skipped and - * there is a one-to-one correspondence between the destination name in the - * provisioner and the physical destination on the broker. - * @param partition the partition to find destination for - * @return destination name for the given partition - */ - String getNameForPartition(int partition); - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProvisioningException.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProvisioningException.java deleted file mode 100644 index a39174e97..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProvisioningException.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2015-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.provisioning; - -import org.springframework.core.NestedRuntimeException; - -/** - * Generic unchecked exception to wrap middleware or technology specific exceptions. - * Wrapped exceptions could be either checked or unchecked. - * - * See {@link NestedRuntimeException} for more usage details. - * - * @author Soby Chacko - */ -@SuppressWarnings("serial") -public class ProvisioningException extends NestedRuntimeException { - - /** - * Constructor that takes a message. - * @param msg the detail message - */ - public ProvisioningException(String msg) { - super(msg); - } - - /** - * Constructor that takes a message and a root cause. - * @param msg the detail message - * @param cause the cause of the exception. This argument is generally expected to be - * middleware specific. - */ - public ProvisioningException(String msg, Throwable cause) { - super(msg, cause); - } - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProvisioningProvider.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProvisioningProvider.java deleted file mode 100644 index a827e3d93..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/provisioning/ProvisioningProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2015-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.provisioning; - -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.binder.ProducerProperties; - -/** - * Provisioning SPI that allows the users to provision destinations such as queues and - * topics. This SPI will allow the binders to be separated from any provisioning concerns - * and only focus on setting up endpoints for sending/receiving messages. - * - * Implementations must implement the following methods: - * - *
    - *
  • {@link #provisionProducerDestination(String, ProducerProperties)}
  • - *
  • {@link #provisionConsumerDestination(String, String, ConsumerProperties)}
  • - *
- * - * @param the consumer properties type - * @param

the producer properties type - * @author Soby Chacko - * @since 1.2 - */ -public interface ProvisioningProvider { - - /** - * Creates middleware destination on the physical broker for the producer to send - * data. The implementation is middleware-specific. - * @param name the name of the producer destination - * @param properties producer properties - * @return reference to {@link ProducerDestination} that represents a producer - * @throws ProvisioningException on underlying provisioning errors from the middleware - */ - ProducerDestination provisionProducerDestination(String name, P properties) - throws ProvisioningException; - - /** - * Creates the middleware destination on the physical broker for the consumer to - * consume data. The implementation is middleware-specific. - * @param name the name of the destination - * @param group the consumer group - * @param properties consumer properties - * @return reference to {@link ConsumerDestination} that represents a consumer - * @throws ProvisioningException on underlying provisioning errors from the middleware - */ - ConsumerDestination provisionConsumerDestination(String name, String group, - C properties) throws ProvisioningException; - -} diff --git a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/reflection/GenericsUtils.java b/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/reflection/GenericsUtils.java deleted file mode 100644 index dbd814a04..000000000 --- a/core/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/reflection/GenericsUtils.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2016-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.reflection; - -import java.lang.reflect.Type; - -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.PollableConsumerBinder; -import org.springframework.cloud.stream.binder.PollableSource; -import org.springframework.core.ResolvableType; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * Internal utilities for handling generics. - * - * @author Marius Bogoevici - * @author Gary Russell - */ -public final class GenericsUtils { - - private GenericsUtils() { - super(); - } - - /** - * For a specific class that implements or extends a parameterized type, return the - * parameter of that interface at a given position. For example, for this class: - * - *

 {@code
-	 * class MessageChannelBinder implements Binder
-	 * } 
- * - *
 {@code
-	 * getParameterType(MessageChannelBinder.class, Binder.class, 0);
-	 * } 
- * - * will return {@code Binder} - * @param evaluatedClass the evaluated class - * @param interfaceClass the parametrized interface - * @param position the position - * @return the parameter type if any - * @throws IllegalStateException if the evaluated class does not implement the - * interface or - */ - public static Class getParameterType(Class evaluatedClass, - Class interfaceClass, int position) { - Class bindableType = null; - Assert.isTrue(interfaceClass.isInterface(), - "'interfaceClass' must be an interface"); - if (!interfaceClass.isAssignableFrom(evaluatedClass)) { - throw new IllegalStateException( - evaluatedClass + " does not implement " + interfaceClass); - } - ResolvableType currentType = ResolvableType.forType(evaluatedClass); - while (!Object.class.equals(currentType.getRawClass()) && bindableType == null) { - ResolvableType[] interfaces = currentType.getInterfaces(); - ResolvableType resolvableType = null; - for (ResolvableType interfaceType : interfaces) { - if (interfaceClass.equals(interfaceType.getRawClass())) { - resolvableType = interfaceType; - break; - } - } - if (resolvableType == null) { - currentType = currentType.getSuperType(); - } - else { - ResolvableType[] generics = resolvableType.getGenerics(); - ResolvableType generic = generics[position]; - Class resolvedParameter = generic.resolve(); - if (resolvedParameter != null) { - bindableType = resolvedParameter; - } - else { - bindableType = Object.class; - } - } - } - if (bindableType == null) { - throw new IllegalStateException( - "Cannot find parameter of " + evaluatedClass.getName() + " for " - + interfaceClass + " at position " + position); - } - return bindableType; - } - - /** - * Return the generic type of PollableSource to determine if it is appropriate for the - * binder. e.g., with PollableMessageSource extends - * PollableSource<MessageHandler> and AbstractMessageChannelBinder implements - * PollableConsumerBinder<MessageHandler, C> We're checking that the the generic - * type (MessageHandler) matches. - * @param binderInstance the binder. - * @param bindingTargetType the binding target type. - * @return true if found, false otherwise. - */ - @SuppressWarnings("rawtypes") - public static boolean checkCompatiblePollableBinder(Binder binderInstance, - Class bindingTargetType) { - Class[] binderInterfaces = ClassUtils.getAllInterfaces(binderInstance); - for (Class intf : binderInterfaces) { - if (PollableConsumerBinder.class.isAssignableFrom(intf)) { - Class[] targetInterfaces = ClassUtils - .getAllInterfacesForClass(bindingTargetType); - Class psType = findPollableSourceType(targetInterfaces); - if (psType != null) { - return getParameterType(binderInstance.getClass(), intf, 0) - .isAssignableFrom(psType); - } - } - } - return false; - } - - private static Class findPollableSourceType(Class[] targetInterfaces) { - for (Class targetIntf : targetInterfaces) { - if (PollableSource.class.isAssignableFrom(targetIntf)) { - Type[] supers = targetIntf.getGenericInterfaces(); - for (Type type : supers) { - ResolvableType resolvableType = ResolvableType.forType(type); - if (resolvableType.getRawClass().equals(PollableSource.class)) { - return resolvableType.getGeneric(0).getRawClass(); - } - } - } - } - return null; - } - -} diff --git a/core/spring-cloud-stream/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/spring-cloud-stream/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index c8cc7ac9c..000000000 --- a/core/spring-cloud-stream/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "properties": [ - { - "defaultValue": "false", - "name": "spring.cloud.stream.override-cloud-connectors", - "description": "This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). The typical usage of this property is to be nested in a customized environment when connecting to multiple systems.", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "true", - "name": "management.health.binders.enabled", - "description": "Allows to enable/disable binder's' health indicators. If you want to disable health indicator completely, then set it to `false`.", - "type": "java.lang.Boolean" - }, - { - "defaultValue": "none", - "name": "spring.cloud.stream.sendto.destination", - "description": "The name of the header used to determine the name of the output destination", - "type": "java.lang.String" - }, - { - "defaultValue": "none", - "name": "spring.cloud.stream.pollable-source", - "description": "A semi-colon delimited list of binding names of pollable sources. Binding names follow the same naming convention as functions. For example, name '...pollable-source=foobar' will be accessible as 'foobar-iin-0'' binding", - "type": "java.lang.String" - } - ] -} diff --git a/core/spring-cloud-stream/src/main/resources/META-INF/shared.beans b/core/spring-cloud-stream/src/main/resources/META-INF/shared.beans deleted file mode 100644 index 92194a7bf..000000000 --- a/core/spring-cloud-stream/src/main/resources/META-INF/shared.beans +++ /dev/null @@ -1,15 +0,0 @@ -org.springframework.boot.autoconfigure.amqp.ConnectionFactoryCustomizer -org.springframework.cloud.stream.config.SpelExpressionConverterConfiguration$SpelConverter -org.springframework.cloud.stream.config.ListenerContainerCustomizer -org.springframework.cloud.stream.binder.kafka.ListenerContainerWithDlqAndRetryCustomizer -org.springframework.cloud.stream.binder.kafka.support.ConsumerConfigCustomizer -org.springframework.cloud.stream.binder.kafka.support.ProducerConfigCustomizer -org.springframework.cloud.stream.binder.kafka.provisioning.AdminClientConfigCustomizer -org.springframework.boot.autoconfigure.kafka.StreamsBuilderFactoryBeanCustomizer -org.springframework.kafka.config.KafkaStreamsCustomizer -org.springframework.rabbit.stream.listener.ConsumerCustomizer -org.springframework.amqp.core.DeclarableCustomizer -org.springframework.cloud.stream.config.ProducerMessageHandlerCustomizer -org.springframework.cloud.stream.binder.test.InputDestination -org.springframework.cloud.stream.binder.test.OutputDestination -org.springframework.cloud.stream.config.BindingHandlerAdvise diff --git a/core/spring-cloud-stream/src/main/resources/META-INF/spring.factories b/core/spring-cloud-stream/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 1a92c5655..000000000 --- a/core/spring-cloud-stream/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,7 +0,0 @@ -org.springframework.boot.env.EnvironmentPostProcessor:\ -org.springframework.cloud.stream.function.RoutingFunctionEnvironmentPostProcessor,\ -org.springframework.cloud.stream.config.PollerConfigEnvironmentPostProcessor -org.springframework.context.ApplicationContextInitializer:\ -org.springframework.cloud.stream.function.PollableSourceInitializer - - diff --git a/core/spring-cloud-stream/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/core/spring-cloud-stream/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index ac95cf95e..000000000 --- a/core/spring-cloud-stream/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,5 +0,0 @@ -org.springframework.cloud.stream.config.BindersHealthIndicatorAutoConfiguration -org.springframework.cloud.stream.config.ChannelsEndpointAutoConfiguration -org.springframework.cloud.stream.config.BindingsEndpointAutoConfiguration -org.springframework.cloud.stream.config.BindingServiceConfiguration -org.springframework.cloud.stream.function.FunctionConfiguration diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderChildContextInitializerTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderChildContextInitializerTests.java deleted file mode 100644 index 59b709905..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderChildContextInitializerTests.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2022-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.aot.AotDetector; -import org.springframework.aot.test.generate.TestGenerationContext; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.context.annotation.UserConfigurations; -import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration; -import org.springframework.cloud.stream.config.BindingServiceConfiguration; -import org.springframework.cloud.stream.function.FunctionConfiguration; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.aot.ApplicationContextAotGenerator; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.log.LogAccessor; -import org.springframework.core.test.tools.CompileWithForkedClassLoader; -import org.springframework.core.test.tools.TestCompiler; -import org.springframework.javapoet.ClassName; -import org.springframework.messaging.MessageChannel; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.mock; - -/** - * Tests for the {@link BinderChildContextInitializer}. - * - * @author Chris Bono - */ -@ExtendWith(OutputCaptureExtension.class) -@Disabled -class BinderChildContextInitializerTests { - - private static final LogAccessor LOG = new LogAccessor(BinderChildContextInitializerTests.class); - - @Test - @CompileWithForkedClassLoader - void shouldStartDefaultBinderChildContextFromAotContributions(CapturedOutput output) { - - // Test description: - // ----------------------- - // Use context runner to create a boostrap context that we can then pass into AOT processor. - // The AOT processor will then generate the ACI for the default binder (no user declared binders). - // We then initialize a fresh app context using the generated ACI and verify the expected output. - - ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class, - BindingServiceConfiguration.class, FunctionConfiguration.class)) - .withInitializer(new ConfigDataApplicationContextInitializer()) - .withConfiguration(UserConfigurations.of(TestFooBinderAppConfiguration.class)); - contextRunner.prepare(context -> { - TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class); - ClassName className = new ApplicationContextAotGenerator().processAheadOfTime( - (GenericApplicationContext) context.getSourceApplicationContext(), generationContext); - generationContext.writeGeneratedContent(); - TestCompiler compiler = TestCompiler.forSystem(); - compiler.with(generationContext).compile(compiled -> { - // Initialize the context w/ the generated ACI - GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); - System.out.println("*** Before ACI init -> OUTPUT: " + output.getAll()); - ApplicationContextInitializer initializer = compiled - .getInstance(ApplicationContextInitializer.class, className.toString()); - initializer.initialize(freshApplicationContext); - System.out.println("*** After ACI init -> OUTPUT: " + output.getAll()); - assertThat(output).contains("Beginning AOT processing for binder child contexts"); - assertThat(output).contains("Pre-creating binder child context (AOT) for mock"); - assertThat(output).contains("Generating AOT child context initializer for mock"); - assertThat(output).contains("Refreshing mock_context"); - - // Refresh the initialized context and verify the binder child contexts are used - TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true") - .applyToSystemProperties(freshApplicationContext::refresh); - assertThat(output).contains("Replacing instance w/ one that uses child context initializers"); - assertThat(output).contains("Setting binder child context initializers on binder factory"); - - // Make sure we can get the binders - DefaultBinderFactory binderFactory = freshApplicationContext.getBean(DefaultBinderFactory.class); - Binder mockBinder = binderFactory.getBinder("mock", MessageChannel.class); - assertThat(mockBinder).isNotNull(); - assertThat(output).contains("Caching the binder: mock"); - - // no default or name given - uses single available binder - assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder); - assertThat(output).contains("No specific name or default given - using single available child initializer 'mock'"); - - assertThatIllegalStateException().isThrownBy( - () -> binderFactory.getBinder("mockBinder1", MessageChannel.class)) - .withMessageContaining("Requested binder 'mockBinder1' did not match available binders"); - - binderFactory.setDefaultBinder("mock"); - assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder); - }); - }); - } - - @Test - @CompileWithForkedClassLoader - @SuppressWarnings("unchecked") - void shouldStartDeclardBinderChildContextsFromAotContributions(CapturedOutput output) { - - // Test description: - // ----------------------- - // Use context runner to create a boostrap context that we can then pass into AOT processor. - // The AOT processor will then generate the ACI for each binder child context defined in the application.yml. - // We then initialize a fresh app context using the generated ACIs and verify the expected output. - ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class, - BindingServiceConfiguration.class, FunctionConfiguration.class)) - .withInitializer(new ConfigDataApplicationContextInitializer()) - .withPropertyValues("spring.config.location=classpath:binder-aot-test/") - .withConfiguration(UserConfigurations.of(TestFooBinderAppConfiguration.class)); - - contextRunner.prepare(context -> { - TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class); - ClassName className = new ApplicationContextAotGenerator().processAheadOfTime( - (GenericApplicationContext) context.getSourceApplicationContext(), generationContext); - generationContext.writeGeneratedContent(); - TestCompiler compiler = TestCompiler.forSystem(); - compiler.with(generationContext).compile(compiled -> { - // Initialize the context w/ the generated ACI - GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); - System.out.println("*** Before ACI init -> OUTPUT: " + output.getAll()); - ApplicationContextInitializer initializer = compiled - .getInstance(ApplicationContextInitializer.class, className.toString()); - initializer.initialize(freshApplicationContext); - System.out.println("*** After ACI init -> OUTPUT: " + output.getAll()); - assertThat(output).contains("Beginning AOT processing for binder child contexts"); - assertThat(output).contains("Pre-creating binder child context (AOT) for mockBinder2"); - assertThat(output).contains("Pre-creating binder child context (AOT) for mockBinder1"); - assertThat(output).contains("Generating AOT child context initializer for mockBinder2"); - assertThat(output).contains("Refreshing mockBinder2_context"); - assertThat(output).contains("Generating AOT child context initializer for mockBinder1"); - assertThat(output).contains("Refreshing mockBinder1_context"); - - // Refresh the initialized context and verify the binder child contexts are used - TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true") - .applyToSystemProperties(freshApplicationContext::refresh); - - assertThat(output).contains("Replacing instance w/ one that uses child context initializers"); - assertThat(output).contains("Setting binder child context initializers on binder factory"); - - // Make sure we can get the binders - DefaultBinderFactory binderFactory = freshApplicationContext.getBean(DefaultBinderFactory.class); - assertThat(binderFactory.getBinder("mockBinder1", MessageChannel.class)).isNotNull(); - assertThat(output).contains("Caching the binder: mockBinder1"); - - Binder mockBinder2 = binderFactory.getBinder("mockBinder2", MessageChannel.class); - assertThat(mockBinder2).isNotNull(); - assertThat(output).contains("Caching the binder: mockBinder2"); - - assertThatIllegalStateException().isThrownBy( - () -> binderFactory.getBinder("mockBinder3", MessageChannel.class)) - .withMessageContaining("Requested binder 'mockBinder3' did not match available binders"); - - assertThatIllegalStateException().isThrownBy( - () -> binderFactory.getBinder(null, MessageChannel.class)) - .withMessageContaining("No specific name or default given - can't determine which binder to use"); - - binderFactory.setDefaultBinder("mockBinder2"); - assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder2); - }); - }); - } - - static class TestTarget { - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - static class TestFooBinderAppConfiguration { - - @Bean - GenericConversionService integrationConversionService() { - return mock(GenericConversionService.class); - } - - @Bean - Supplier fooSource() { - return () -> "foo-" + System.currentTimeMillis(); - } - - @Bean - Consumer fooSink() { - return (foo) -> LOG.info("*** FOO: " + foo); - } - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BindingTargetTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BindingTargetTests.java deleted file mode 100644 index e4108f482..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BindingTargetTests.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.PropertySource; -import org.springframework.messaging.MessageChannel; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * @author Marius Bogoevici - * @author Janne Valkealahti - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = BindingTargetTests.TestFooChannels.class, properties = {"spring.cloud.stream.default-binder=mock", - "spring.cloud.function.definition=process1;process2"}) -public class BindingTargetTests { - - @Autowired - private BinderFactory binderFactory; - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Test - void testArbitraryInterfaceChannelsBound() { - Binder binder = this.binderFactory.getBinder(null, MessageChannel.class); - verify(binder).bindConsumer(eq("someQueue.0"), isNull(), - Mockito.any(MessageChannel.class), Mockito.any()); - verify(binder).bindConsumer(eq("someQueue.2"), isNull(), - Mockito.any(MessageChannel.class), Mockito.any()); - verify(binder).bindProducer(eq("someQueue.1"), Mockito.any(MessageChannel.class), - Mockito.any()); - verify(binder).bindProducer(eq("someQueue.3"), Mockito.any(MessageChannel.class), - Mockito.any()); - verifyNoMoreInteractions(binder); - } - - @EnableAutoConfiguration - @PropertySource("classpath:/org/springframework/cloud/stream/binder/arbitrary-binding-test.properties") - public static class TestFooChannels { - - @Bean - public Function process1() { - return s -> s; - } - - @Bean - public Function process2() { - return s -> s; - } - - } -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderFactoryTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderFactoryTests.java deleted file mode 100644 index d6edc2328..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderFactoryTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link DefaultBinderFactory} - * - * @author Chris Bono - */ -class DefaultBinderFactoryTests { - - @Test - void updateBinderConfigurations() { - Map binderConfigs = new HashMap<>(); - binderConfigs.put("foo", mock(BinderConfiguration.class)); - DefaultBinderFactory binderFactory = new DefaultBinderFactory(binderConfigs, null, null); - - Map newBinderConfigs = new HashMap<>(); - newBinderConfigs.put("bar", mock(BinderConfiguration.class)); - binderFactory.updateBinderConfigurations(newBinderConfigs); - - assertThat(binderFactory.getBinderConfigurations()).containsExactlyInAnyOrderEntriesOf(newBinderConfigs); - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderTests.java deleted file mode 100644 index 75f20ba37..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/DefaultBinderTests.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2023-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import org.junit.jupiter.api.Test; - -import org.springframework.context.Lifecycle; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Myeonghyeon Lee - * - */ -public class DefaultBinderTests { - - @Test - void testDefaultBinderIsPaused() { - @SuppressWarnings({ "rawtypes" }) - DefaultBinding binding = new DefaultBinding<>("foo", "bar", "target", (Binding) () -> { - }); - - assertThat(binding.isPaused()).isFalse(); - - binding.pause(); - - assertThat(binding.isPaused()).isTrue(); - } - - @Test - void testDefaultBinderIsNull() { - DefaultBinding binding = new DefaultBinding<>("foo", "bar", "target", null); - assertThat(binding.getState()).isNotNull(); - } - - @Test - void testDefaultBinderIsRunning() { - DefaultBinding binding = new DefaultBinding<>("foo", "bar", "target", new Lifecycle() { - private Boolean running = false; - @Override - public void start() { - running = !this.running; - } - - @Override - public void stop() { - running = !this.running; - } - - @Override - public boolean isRunning() { - return running; - } - }); - binding.start(); - assertThat(binding.isRunning()).isTrue(); - } -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ExtendedPropertiesDefaultTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ExtendedPropertiesDefaultTests.java deleted file mode 100644 index 69654b13b..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ExtendedPropertiesDefaultTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binder; - -import java.util.Collections; -import java.util.HashMap; - -import org.junit.jupiter.api.Test; - -import org.springframework.cloud.stream.utils.FooConsumerProperties; -import org.springframework.cloud.stream.utils.FooProducerProperties; -import org.springframework.cloud.stream.utils.MockExtendedBinderConfiguration; -import org.springframework.messaging.MessageChannel; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Soby Chacko - */ -public class ExtendedPropertiesDefaultTests { - - @Test - void testExtendedDefaultProducerProperties() { - DefaultBinderFactory binderFactory = createMockExtendedBinderFactory(); - Binder binder = binderFactory.getBinder(null, - MessageChannel.class); - FooProducerProperties fooProducerProperties = (FooProducerProperties) ((ExtendedPropertiesBinder) binder) - .getExtendedProducerProperties("output"); - // Expectations are set in the mock configuration for the binder factory - assertThat(fooProducerProperties.getExtendedProperty()) - .isEqualTo("someFancyExtension"); - } - - @Test - void testExtendedDefaultConsumerProperties() { - DefaultBinderFactory binderFactory = createMockExtendedBinderFactory(); - Binder binder = binderFactory.getBinder(null, - MessageChannel.class); - FooConsumerProperties fooConsumerProperties = (FooConsumerProperties) ((ExtendedPropertiesBinder) binder) - .getExtendedConsumerProperties("input"); - // Expectations are set in the mock configuration for the binder factory - assertThat(fooConsumerProperties.getExtendedProperty()) - .isEqualTo("someFancyExtension"); - } - - private DefaultBinderFactory createMockExtendedBinderFactory() { - BinderTypeRegistry binderTypeRegistry = createMockExtendedBinderTypeRegistry(); - return new DefaultBinderFactory( - Collections.singletonMap("mock", - new BinderConfiguration("mock", new HashMap<>(), true, true)), - binderTypeRegistry, null); - } - - private DefaultBinderTypeRegistry createMockExtendedBinderTypeRegistry() { - return new DefaultBinderTypeRegistry( - Collections.singletonMap("mock", new BinderType("mock", - new Class[] { MockExtendedBinderConfiguration.class }))); - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java deleted file mode 100644 index 808a8244b..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.SmartLifecycle; -import org.springframework.context.annotation.Bean; -import org.springframework.messaging.MessageChannel; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Janne Valkealahti - * @author Soby Chacko - */ -public class InputOutputBindingOrderTest { - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testInputOutputBindingOrder() { - ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder( - TestSource.class).web(WebApplicationType.NONE).run( - "--spring.cloud.stream.defaultBinder=mock", - "--spring.jmx.enabled=false"); - Binder binder = applicationContext.getBean(BinderFactory.class).getBinder(null, - MessageChannel.class); - // input is bound after the context has been started - verify(binder).bindConsumer(eq("processor-in-0"), isNull(), Mockito.any(MessageChannel.class), - Mockito.any()); - SomeLifecycle someLifecycle = applicationContext.getBean(SomeLifecycle.class); - assertThat(someLifecycle.isRunning()); - applicationContext.close(); - assertThat(someLifecycle.isRunning()).isFalse(); - applicationContext.close(); - } - - @EnableAutoConfiguration - public static class TestSource { - - @Bean - public SomeLifecycle someLifecycle() { - return new SomeLifecycle(); - } - - @Bean - public Function processor() { - return s -> s; - } - - } - - public static class SomeLifecycle implements SmartLifecycle { - - @Autowired - private BinderFactory binderFactory; - - private boolean running; - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public synchronized void start() { - Binder binder = this.binderFactory.getBinder(null, MessageChannel.class); - verify(binder).bindProducer(eq("processor-out-0"), Mockito.any(MessageChannel.class), - Mockito.any()); - // input was not bound yet - verifyNoMoreInteractions(binder); - this.running = true; - } - - @Override - public synchronized void stop() { - this.running = false; - } - - @Override - public synchronized boolean isRunning() { - return this.running; - } - - @Override - public boolean isAutoStartup() { - return true; - } - - @Override - public void stop(Runnable callback) { - stop(); - if (callback != null) { - callback.run(); - } - } - - @Override - public int getPhase() { - return 0; - } - - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/MessageConverterTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/MessageConverterTests.java deleted file mode 100644 index b77286e8a..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/MessageConverterTests.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2002-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder; - -import java.nio.BufferUnderflowException; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.GenericMessage; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -/** - * @author Gary Russell - * @author Ilayaperumal Gopinathan - * @since 1.0 - */ -public class MessageConverterTests { - - @Test - void testHeaderEmbedding() throws Exception { - Message message = MessageBuilder.withPayload("Hello".getBytes()) - .setHeader("foo", "bar").setHeader("baz", "quxx").build(); - byte[] embedded = EmbeddedHeaderUtils.embedHeaders(new MessageValues(message), - "foo", "baz"); - assertThat(embedded[0] & 0xff).isEqualTo(0xff); - assertThat(new String(embedded).substring(1)).isEqualTo( - "\u0002\u0003foo\u0000\u0000\u0000\u0005\"bar\"\u0003baz\u0000\u0000\u0000\u0006\"quxx\"Hello"); - - MessageValues extracted = EmbeddedHeaderUtils - .extractHeaders(MessageBuilder.withPayload(embedded).build(), false); - assertThat(new String((byte[]) extracted.getPayload())).isEqualTo("Hello"); - assertThat(extracted.get("foo")).isEqualTo("bar"); - assertThat(extracted.get("baz")).isEqualTo("quxx"); - } - - @Test - void testConfigurableHeaders() throws Exception { - Message message = MessageBuilder.withPayload("Hello".getBytes()) - .setHeader("foo", "bar").setHeader("baz", "quxx") - .setHeader("contentType", "text/plain").build(); - String[] headers = new String[] { "foo" }; - byte[] embedded = EmbeddedHeaderUtils.embedHeaders(new MessageValues(message), - EmbeddedHeaderUtils.headersToEmbed(headers)); - assertThat(embedded[0] & 0xff).isEqualTo(0xff); - assertThat(new String(embedded).substring(1)).isEqualTo( - "\u0002\u000BcontentType\u0000\u0000\u0000\u000C\"text/plain\"\u0003foo\u0000\u0000\u0000\u0005\"bar\"Hello"); - MessageValues extracted = EmbeddedHeaderUtils - .extractHeaders(MessageBuilder.withPayload(embedded).build(), false); - assertThat(new String((byte[]) extracted.getPayload())).isEqualTo("Hello"); - assertThat(extracted.get("foo")).isEqualTo("bar"); - assertThat(extracted.get("baz")).isNull(); - assertThat(extracted.get("contentType")).isEqualTo("text/plain"); - assertThat(extracted.get("timestamp")).isNull(); - MessageValues extractedWithRequestHeaders = EmbeddedHeaderUtils - .extractHeaders(MessageBuilder.withPayload(embedded).build(), true); - assertThat(extractedWithRequestHeaders.get("foo")).isEqualTo("bar"); - assertThat(extractedWithRequestHeaders.get("baz")).isNull(); - assertThat(extractedWithRequestHeaders.get("contentType")) - .isEqualTo("text/plain"); - assertThat(extractedWithRequestHeaders.get("timestamp")).isNotNull(); - } - - @Test - void testHeaderExtractionWithDirectPayload() throws Exception { - Message message = MessageBuilder.withPayload("Hello".getBytes()) - .setHeader("foo", "bar").setHeader("baz", "quxx").build(); - byte[] embedded = EmbeddedHeaderUtils.embedHeaders(new MessageValues(message), - "foo", "baz"); - assertThat(embedded[0] & 0xff).isEqualTo(0xff); - assertThat(new String(embedded).substring(1)).isEqualTo( - "\u0002\u0003foo\u0000\u0000\u0000\u0005\"bar\"\u0003baz\u0000\u0000\u0000\u0006\"quxx\"Hello"); - - MessageValues extracted = EmbeddedHeaderUtils.extractHeaders(embedded); - assertThat(new String((byte[]) extracted.getPayload())).isEqualTo("Hello"); - assertThat(extracted.get("foo")).isEqualTo("bar"); - assertThat(extracted.get("baz")).isEqualTo("quxx"); - } - - @Test - void testUnicodeHeader() throws Exception { - Message message = MessageBuilder.withPayload("Hello".getBytes()) - .setHeader("foo", "bar").setHeader("baz", "ØØØØØØØØ").build(); - byte[] embedded = EmbeddedHeaderUtils.embedHeaders(new MessageValues(message), - "foo", "baz"); - assertThat(embedded[0] & 0xff).isEqualTo(0xff); - assertThat(new String(embedded, StandardCharsets.UTF_8).substring(1)).isEqualTo( - "\u0002\u0003foo\u0000\u0000\u0000\u0005\"bar\"\u0003baz\u0000\u0000\u0000\u0012\"ØØØØØØØØ\"Hello"); - - MessageValues extracted = EmbeddedHeaderUtils - .extractHeaders(MessageBuilder.withPayload(embedded).build(), false); - assertThat(new String((byte[]) extracted.getPayload())).isEqualTo("Hello"); - assertThat(extracted.get("foo")).isEqualTo("bar"); - assertThat(extracted.get("baz")).isEqualTo("ØØØØØØØØ"); - } - - @Test - void testHeaderEmbeddingMissingHeader() throws Exception { - Message message = MessageBuilder.withPayload("Hello".getBytes()) - .setHeader("foo", "bar").build(); - byte[] embedded = EmbeddedHeaderUtils.embedHeaders(new MessageValues(message), - "foo", "baz"); - assertThat(embedded[0] & 0xff).isEqualTo(0xff); - assertThat(new String(embedded).substring(1)) - .isEqualTo("\u0001\u0003foo\u0000\u0000\u0000\u0005\"bar\"Hello"); - } - - @Test - void testBadDecode() throws Exception { - byte[] bytes = new byte[] { (byte) 0xff, 99 }; - Message message = new GenericMessage<>(bytes); - try { - EmbeddedHeaderUtils.extractHeaders(message, false); - fail("Exception expected"); - } - catch (Exception e) { - String s = EmbeddedHeaderUtils.decodeExceptionMessage(message); - assertThat(e).isInstanceOf(BufferUnderflowException.class); - } - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingWithDefaultsTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingWithDefaultsTests.java deleted file mode 100644 index c547c1f08..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingWithDefaultsTests.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.binder; - -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -import org.springframework.messaging.MessageChannel; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * @author Marius Bogoevici - * @author Janne Valkealahti - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = SinkBindingWithDefaultsTests.TestSink.class, properties = "spring.cloud.stream.defaultBinder=mock") -public class SinkBindingWithDefaultsTests { - - @Autowired - private BinderFactory binderFactory; - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testSinkInputChannelBound() { - Binder binder = this.binderFactory.getBinder(null, MessageChannel.class); - verify(binder).bindConsumer(eq("consumer-in-0"), isNull(), Mockito.any(MessageChannel.class), - Mockito.any()); - verifyNoMoreInteractions(binder); - } - - @EnableAutoConfiguration - public static class TestSink { - - @Bean - public Consumer consumer() { - return s -> System.out.println(); - } - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/BindingLifecycleTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/BindingLifecycleTests.java deleted file mode 100644 index 2e301557c..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/BindingLifecycleTests.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.binding; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.cloud.stream.binder.Binding; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * @author Oleg Zhurakousky - * - */ -@SuppressWarnings("unchecked") -public class BindingLifecycleTests { - - @Test - void testInputBindingLifecycle() { - Map bindables = new HashMap<>(); - - Bindable bindableWithTwo = new Bindable() { - public Collection> createAndBindInputs( - BindingService adapter) { - return Arrays.asList(mock(Binding.class), mock(Binding.class)); - } - }; - Bindable bindableWithThree = new Bindable() { - public Collection> createAndBindInputs( - BindingService adapter) { - return Arrays.asList(mock(Binding.class), mock(Binding.class), - mock(Binding.class)); - } - }; - Bindable bindableEmpty = new Bindable() { - }; - - bindables.put("two", bindableWithTwo); - bindables.put("empty", bindableEmpty); - bindables.put("three", bindableWithThree); - - InputBindingLifecycle lifecycle = new InputBindingLifecycle( - mock(BindingService.class), bindables); - lifecycle.start(); - - Collection> lifecycleInputBindings = (Collection>) new DirectFieldAccessor( - lifecycle).getPropertyValue("inputBindings"); - assertThat(lifecycleInputBindings.size() == 5).isTrue(); - lifecycle.stop(); - } - - @Test - void testOutputBindingLifecycle() { - Map bindables = new HashMap<>(); - - Bindable bindableWithTwo = new Bindable() { - public Collection> createAndBindOutputs( - BindingService adapter) { - return Arrays.asList(mock(Binding.class), mock(Binding.class)); - } - }; - Bindable bindableWithThree = new Bindable() { - public Collection> createAndBindOutputs( - BindingService adapter) { - return Arrays.asList(mock(Binding.class), mock(Binding.class), - mock(Binding.class)); - } - }; - Bindable bindableEmpty = new Bindable() { - }; - - bindables.put("two", bindableWithTwo); - bindables.put("empty", bindableEmpty); - bindables.put("three", bindableWithThree); - - OutputBindingLifecycle lifecycle = new OutputBindingLifecycle( - mock(BindingService.class), bindables); - lifecycle.start(); - - Collection> lifecycleOutputBindings = (Collection>) new DirectFieldAccessor( - lifecycle).getPropertyValue("outputBindings"); - assertThat(lifecycleOutputBindings.size() == 5).isTrue(); - lifecycle.stop(); - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/CustomPartitionedProducerTest.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/CustomPartitionedProducerTest.java deleted file mode 100644 index 8db7c861f..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/CustomPartitionedProducerTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binding; - -import java.lang.reflect.Field; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.stream.binder.PartitionHandler; -import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy; -import org.springframework.cloud.stream.binder.PartitionSelectorStrategy; -import org.springframework.cloud.stream.partitioning.CustomPartitionKeyExtractorClass; -import org.springframework.cloud.stream.partitioning.CustomPartitionSelectorClass; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.PropertySource; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Ilayaperumal Gopinathan - * @author Oleg Zhurakousky - * @author Soby Chacko - */ -public class CustomPartitionedProducerTest { - - @Test - void testCustomPartitionedProducer() { - ApplicationContext context = SpringApplication.run( - CustomPartitionedProducerTest.TestSource.class, - "--spring.cloud.stream.output-bindings=output", - "--spring.jmx.enabled=false", "--spring.main.web-application-type=none", - "--spring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass=" - + "org.springframework.cloud.stream.partitioning.CustomPartitionKeyExtractorClass", - "--spring.cloud.stream.bindings.output.producer.partitionSelectorClass=" - + "org.springframework.cloud.stream.partitioning.CustomPartitionSelectorClass", - "--spring.cloud.stream.default-binder=mock"); - DirectChannel messageChannel = context.getBean("output", DirectChannel.class); - for (ChannelInterceptor channelInterceptor : messageChannel - .getInterceptors()) { - if (channelInterceptor instanceof MessageConverterConfigurer.PartitioningInterceptor) { - Field partitionHandlerField = ReflectionUtils.findField( - MessageConverterConfigurer.PartitioningInterceptor.class, - "partitionHandler"); - ReflectionUtils.makeAccessible(partitionHandlerField); - PartitionHandler partitionHandler = (PartitionHandler) ReflectionUtils - .getField(partitionHandlerField, channelInterceptor); - Field partitonKeyExtractorField = ReflectionUtils.findField( - PartitionHandler.class, "partitionKeyExtractorStrategy"); - ReflectionUtils.makeAccessible(partitonKeyExtractorField); - Field partitonSelectorField = ReflectionUtils - .findField(PartitionHandler.class, "partitionSelectorStrategy"); - ReflectionUtils.makeAccessible(partitonSelectorField); - assertThat(((PartitionKeyExtractorStrategy) ReflectionUtils - .getField(partitonKeyExtractorField, partitionHandler)).getClass() - .equals(CustomPartitionKeyExtractorClass.class)).isTrue(); - assertThat(((PartitionSelectorStrategy) ReflectionUtils - .getField(partitonSelectorField, partitionHandler)).getClass() - .equals(CustomPartitionSelectorClass.class)).isTrue(); - } - } - } - - @Test - void testCustomPartitionedProducerByName() { - ApplicationContext context = SpringApplication.run( - CustomPartitionedProducerTest.TestSource.class, - "--spring.cloud.stream.output-bindings=output", - "--spring.jmx.enabled=false", "--spring.main.web-application-type=none", - "--spring.cloud.stream.bindings.output.producer.partitionKeyExtractorName=customPartitionKeyExtractor", - "--spring.cloud.stream.bindings.output.producer.partitionSelectorName=customPartitionSelector", - "--spring.cloud.stream.default-binder=mock"); - DirectChannel messageChannel = context.getBean("output", DirectChannel.class); - for (ChannelInterceptor channelInterceptor : messageChannel - .getInterceptors()) { - if (channelInterceptor instanceof MessageConverterConfigurer.PartitioningInterceptor) { - Field partitionHandlerField = ReflectionUtils.findField( - MessageConverterConfigurer.PartitioningInterceptor.class, - "partitionHandler"); - ReflectionUtils.makeAccessible(partitionHandlerField); - PartitionHandler partitionHandler = (PartitionHandler) ReflectionUtils - .getField(partitionHandlerField, channelInterceptor); - Field partitonKeyExtractorField = ReflectionUtils.findField( - PartitionHandler.class, "partitionKeyExtractorStrategy"); - ReflectionUtils.makeAccessible(partitonKeyExtractorField); - Field partitonSelectorField = ReflectionUtils - .findField(PartitionHandler.class, "partitionSelectorStrategy"); - ReflectionUtils.makeAccessible(partitonSelectorField); - assertThat(((PartitionKeyExtractorStrategy) ReflectionUtils - .getField(partitonKeyExtractorField, partitionHandler)).getClass() - .equals(CustomPartitionKeyExtractorClass.class)).isTrue(); - assertThat(((PartitionSelectorStrategy) ReflectionUtils - .getField(partitonSelectorField, partitionHandler)).getClass() - .equals(CustomPartitionSelectorClass.class)).isTrue(); - } - } - } - - @Test - void testCustomPartitionedProducerAsSingletons() { - ApplicationContext context = SpringApplication.run( - CustomPartitionedProducerTest.TestSource.class, - "--spring.cloud.stream.output-bindings=output", - "--spring.jmx.enabled=false", "--spring.main.web-application-type=none", - "--spring.cloud.stream.default-binder=mock"); - DirectChannel messageChannel = context.getBean("output", DirectChannel.class); - for (ChannelInterceptor channelInterceptor : messageChannel - .getInterceptors()) { - if (channelInterceptor instanceof MessageConverterConfigurer.PartitioningInterceptor) { - Field partitionHandlerField = ReflectionUtils.findField( - MessageConverterConfigurer.PartitioningInterceptor.class, - "partitionHandler"); - ReflectionUtils.makeAccessible(partitionHandlerField); - PartitionHandler partitionHandler = (PartitionHandler) ReflectionUtils - .getField(partitionHandlerField, channelInterceptor); - Field partitonKeyExtractorField = ReflectionUtils.findField( - PartitionHandler.class, "partitionKeyExtractorStrategy"); - ReflectionUtils.makeAccessible(partitonKeyExtractorField); - Field partitonSelectorField = ReflectionUtils - .findField(PartitionHandler.class, "partitionSelectorStrategy"); - ReflectionUtils.makeAccessible(partitonSelectorField); - assertThat(((PartitionKeyExtractorStrategy) ReflectionUtils - .getField(partitonKeyExtractorField, partitionHandler)).getClass() - .equals(CustomPartitionKeyExtractorClass.class)).isTrue(); - assertThat(((PartitionSelectorStrategy) ReflectionUtils - .getField(partitonSelectorField, partitionHandler)).getClass() - .equals(CustomPartitionSelectorClass.class)).isTrue(); - } - } - } - - @Test - public void testCustomPartitionedProducerMultipleInstances() { - ApplicationContext context = SpringApplication.run( - CustomPartitionedProducerTest.TestSourceMultipleStrategies.class, - "--spring.cloud.stream.output-bindings=output", - "--spring.jmx.enabled=false", "--spring.main.web-application-type=none", - "--spring.cloud.stream.bindings.output.producer.partitionKeyExtractorName=customPartitionKeyExtractorOne", - "--spring.cloud.stream.bindings.output.producer.partitionSelectorName=customPartitionSelectorTwo", - "--spring.cloud.stream.default-binder=mock"); - DirectChannel messageChannel = context.getBean("output", DirectChannel.class); - for (ChannelInterceptor channelInterceptor : messageChannel - .getInterceptors()) { - if (channelInterceptor instanceof MessageConverterConfigurer.PartitioningInterceptor) { - Field partitionHandlerField = ReflectionUtils.findField( - MessageConverterConfigurer.PartitioningInterceptor.class, - "partitionHandler"); - ReflectionUtils.makeAccessible(partitionHandlerField); - PartitionHandler partitionHandler = (PartitionHandler) ReflectionUtils - .getField(partitionHandlerField, channelInterceptor); - Field partitonKeyExtractorField = ReflectionUtils.findField( - PartitionHandler.class, "partitionKeyExtractorStrategy"); - ReflectionUtils.makeAccessible(partitonKeyExtractorField); - Field partitonSelectorField = ReflectionUtils - .findField(PartitionHandler.class, "partitionSelectorStrategy"); - ReflectionUtils.makeAccessible(partitonSelectorField); - assertThat(((PartitionKeyExtractorStrategy) ReflectionUtils - .getField(partitonKeyExtractorField, partitionHandler)).getClass() - .equals(CustomPartitionKeyExtractorClass.class)).isTrue(); - assertThat(((PartitionSelectorStrategy) ReflectionUtils - .getField(partitonSelectorField, partitionHandler)).getClass() - .equals(CustomPartitionSelectorClass.class)).isTrue(); - } - } - } - - @EnableAutoConfiguration - @PropertySource("classpath:/org/springframework/cloud/stream/binder/custom-partitioned-producer-test.properties") - public static class TestSource { - - @Bean - public CustomPartitionSelectorClass customPartitionSelector() { - return new CustomPartitionSelectorClass(); - } - - @Bean - public CustomPartitionKeyExtractorClass customPartitionKeyExtractor() { - return new CustomPartitionKeyExtractorClass(); - } - - } - - @EnableAutoConfiguration - @PropertySource("classpath:/org/springframework/cloud/stream/binder/custom-partitioned-producer-test.properties") - public static class TestSourceMultipleStrategies { - - @Bean - public CustomPartitionSelectorClass customPartitionSelectorOne() { - return new CustomPartitionSelectorClass(); - } - - @Bean - public CustomPartitionSelectorClass customPartitionSelectorTwo() { - return new CustomPartitionSelectorClass(); - } - - @Bean - public CustomPartitionKeyExtractorClass customPartitionKeyExtractorOne() { - return new CustomPartitionKeyExtractorClass(); - } - - @Bean - public CustomPartitionKeyExtractorClass customPartitionKeyExtractorTwo() { - return new CustomPartitionKeyExtractorClass(); - } - - } -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/MessageConverterConfigurerTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/MessageConverterConfigurerTests.java deleted file mode 100644 index 4a20ffa5c..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/MessageConverterConfigurerTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.binding; - -import java.util.Collections; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.cloud.stream.config.BindingProperties; -import org.springframework.cloud.stream.config.BindingServiceProperties; -import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.AbstractMessageConverter; -import org.springframework.messaging.converter.MessageConversionException; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -/** - * @author Gary Russell - * @author Oleg Zhurakousky - * @since 1.3 - * - */ -public class MessageConverterConfigurerTests { - -// @Test - void testConfigureOutputChannelWithBadContentType() { - BindingServiceProperties props = new BindingServiceProperties(); - BindingProperties bindingProps = new BindingProperties(); - bindingProps.setContentType("application/json"); - props.setBindings(Collections.singletonMap("foo", bindingProps)); - CompositeMessageConverterFactory converterFactory = new CompositeMessageConverterFactory( - Collections.emptyList(), null, null); - MessageConverterConfigurer configurer = new MessageConverterConfigurer(props, - converterFactory.getMessageConverterForAllRegistered()); - QueueChannel out = new QueueChannel(); - configurer.configureOutputChannel(out, "foo"); - out.send(new GenericMessage(new Foo(), Collections - .singletonMap(MessageHeaders.CONTENT_TYPE, "bad/ct"))); - Message received = out.receive(0); - assertThat(received).isNotNull(); - assertThat(received.getPayload()).isInstanceOf(Foo.class); - } - - @Test - @Disabled - void testConfigureOutputChannelCannotConvert() { - BindingServiceProperties props = new BindingServiceProperties(); - BindingProperties bindingProps = new BindingProperties(); - bindingProps.setContentType("foo/bar"); - props.setBindings(Collections.singletonMap("foo", bindingProps)); - MessageConverter converter = new AbstractMessageConverter( - new MimeType("foo", "bar")) { - - @Override - protected boolean supports(Class clazz) { - return true; - } - - @Override - protected Object convertToInternal(Object payload, MessageHeaders headers, - Object conversionHint) { - return null; - } - - }; - CompositeMessageConverterFactory converterFactory = new CompositeMessageConverterFactory( - Collections.singletonList(converter), null, null); - MessageConverterConfigurer configurer = new MessageConverterConfigurer(props, - converterFactory.getMessageConverterForAllRegistered()); - QueueChannel out = new QueueChannel(); - configurer.configureOutputChannel(out, "foo"); - try { - out.send(new GenericMessage(new Foo(), - Collections.singletonMap(MessageHeaders.CONTENT_TYPE, - "bad/ct"))); - fail("Expected MessageConversionException: " + out.receive(0)); - } - catch (MessageConversionException e) { - assertThat(e.getMessage()) - .endsWith("to the configured output type: 'foo/bar'"); - } - } - - public static class Foo { - - private String bar = "bar"; - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/config/ArgumentResolversTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/config/ArgumentResolversTests.java deleted file mode 100644 index 85de21b38..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/config/ArgumentResolversTests.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2019-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.cloud.stream.config; - -import java.lang.reflect.Method; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.core.MethodParameter; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.converter.MessageConverter; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * - * @author Oleg Zhurakousky - * - */ -public class ArgumentResolversTests { - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test - void testSmartPayloadArgumentResolver() throws Exception { - - SmartPayloadArgumentResolver resolver = new SmartPayloadArgumentResolver(new TestMessageConverter()); - - Object payload = "hello".getBytes(); - GenericMessage message = new GenericMessage(payload); - MethodParameter parameter = new MethodParameter(getMethod("byteArray", byte[].class), 0); - Object resolvedArgument = resolver.resolveArgument(parameter, message); - assertThat(resolvedArgument).isSameAs(payload); - - parameter = new MethodParameter(getMethod("object", Object.class), 0); - resolvedArgument = resolver.resolveArgument(parameter, message); - assertThat(resolvedArgument).isInstanceOf(Message.class); - - payload = new LinkedHashMap<>(); - message = new GenericMessage(payload); - parameter = new MethodParameter(getMethod("map", Map.class), 0); - resolvedArgument = resolver.resolveArgument(parameter, message); - assertThat(resolvedArgument).isSameAs(payload); - - parameter = new MethodParameter(getMethod("object", Object.class), 0); - resolvedArgument = resolver.resolveArgument(parameter, message); - assertThat(resolvedArgument).isInstanceOf(Message.class); - } - - private Method getMethod(String name, Class parameter) { - return ReflectionUtils.findMethod(this.getClass(), name, parameter); - } - - public void byteArray(byte[] p) { - - } - - public void byteArrayMessage(Message p) { - - } - - public void object(Object p) { - - } - @SuppressWarnings("rawtypes") - public void map(Map p) { - - } - - /* - * The whole point of this converter is to return something other - * then what is being resolved to simply validate when it is invoked - * vs. when it is not. - */ - private static class TestMessageConverter implements MessageConverter { - - @Override - public Object fromMessage(Message message, Class targetClass) { - return message; - } - - @Override - public Message toMessage(Object payload, MessageHeaders headers) { - return new GenericMessage<>(payload); - } - - } -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/config/SpelExpressionConverterConfigurationTests.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/config/SpelExpressionConverterConfigurationTests.java deleted file mode 100644 index 73fa2a5db..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/config/SpelExpressionConverterConfigurationTests.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2015-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.config; - -import java.util.List; - -import jakarta.annotation.PostConstruct; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.PropertyAccessor; -import org.springframework.integration.expression.ExpressionUtils; -import org.springframework.integration.json.JsonPropertyAccessor; -import org.springframework.integration.test.util.TestUtils; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for SpelExpressionConverterConfiguration. - * - * @author Eric Bottard - * @author Artem Bilan - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = SpelExpressionConverterConfigurationTests.Config.class, properties = { - "expression: a.b" }) -public class SpelExpressionConverterConfigurationTests { - - @Autowired - private Pojo pojo; - - @Autowired - private EvaluationContext evaluationContext; - - @Autowired - private Config config; - - @Test - @SuppressWarnings("unchecked") - void converterCorrectlyInstalled() { - Expression expression = this.pojo.getExpression(); - assertThat(expression.getValue("{\"a\": {\"b\": 5}}").toString()).isEqualTo("5"); - - List propertyAccessors = TestUtils.getPropertyValue( - this.evaluationContext, "propertyAccessors", List.class); - - assertThat(propertyAccessors) - .hasAtLeastOneElementOfType(JsonPropertyAccessor.class); - - propertyAccessors = TestUtils.getPropertyValue(this.config.evaluationContext, - "propertyAccessors", List.class); - - assertThat(propertyAccessors) - .hasAtLeastOneElementOfType(JsonPropertyAccessor.class); - } - - @ConfigurationProperties - public static class Pojo { - - private Expression expression; - - public Expression getExpression() { - return this.expression; - } - - public void setExpression(Expression expression) { - this.expression = expression; - } - - } - - @Configuration - @EnableAutoConfiguration - @EnableConfigurationProperties(Pojo.class) - public static class Config implements BeanFactoryAware { - - private BeanFactory beanFactory; - - private EvaluationContext evaluationContext; - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - @Bean - public EvaluationContext evaluationContext() { - return ExpressionUtils.createStandardEvaluationContext(this.beanFactory); - } - - @PostConstruct - public void setup() { - this.evaluationContext = ExpressionUtils - .createStandardEvaluationContext(this.beanFactory); - } - } -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/CustomPartitionKeyExtractorClass.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/CustomPartitionKeyExtractorClass.java deleted file mode 100644 index fcfcf26f9..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/CustomPartitionKeyExtractorClass.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.partitioning; - -import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy; -import org.springframework.messaging.Message; - -/** - * @author Ilayaperumal Gopinathan - */ -public class CustomPartitionKeyExtractorClass implements PartitionKeyExtractorStrategy { - - @Override - public String extractKey(Message message) { - return (String) message.getHeaders().get("key"); - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/CustomPartitionSelectorClass.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/CustomPartitionSelectorClass.java deleted file mode 100644 index db5447b8a..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/CustomPartitionSelectorClass.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017-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.cloud.stream.partitioning; - -import org.springframework.cloud.stream.binder.PartitionSelectorStrategy; - -/** - * @author Ilayaperumal Gopinathan - */ -public class CustomPartitionSelectorClass implements PartitionSelectorStrategy { - - @Override - public int selectPartition(Object key, int partitionCount) { - return Integer.valueOf((String) key); - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java deleted file mode 100644 index f38737f28..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.partitioning; - -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.ConsumerProperties; -import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration; -import org.springframework.cloud.stream.messaging.DirectWithAttributesChannel; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.messaging.MessageChannel; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * @author Marius Bogoevici - * @author Ilayaperumal Gopinathan - * @author Janne Valkealahti - * @author Soby Chacko - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(classes = PartitionedConsumerTest.TestSink.class, properties = "spring.cloud.stream.default-binder=mock") -public class PartitionedConsumerTest { - - @Autowired - private BinderFactory binderFactory; - - @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) - void testBindingPartitionedConsumer() { - Binder binder = this.binderFactory.getBinder(null, MessageChannel.class); - ArgumentCaptor argumentCaptor = ArgumentCaptor - .forClass(ConsumerProperties.class); - ArgumentCaptor directWithAttributesChannelArgumentCaptor = - ArgumentCaptor.forClass(DirectWithAttributesChannel.class); - verify(binder).bindConsumer(eq("partIn"), isNull(), directWithAttributesChannelArgumentCaptor.capture(), - argumentCaptor.capture()); - assertThat(directWithAttributesChannelArgumentCaptor.getValue().getBeanName()).isEqualTo("sink-in-0"); - assertThat(argumentCaptor.getValue().getInstanceIndex()).isEqualTo(0); - assertThat(argumentCaptor.getValue().getInstanceCount()).isEqualTo(2); - verifyNoMoreInteractions(binder); - } - - @EnableAutoConfiguration - @Import({ BinderFactoryAutoConfiguration.class }) - @PropertySource("classpath:/org/springframework/cloud/stream/binder/partitioned-consumer-test.properties") - public static class TestSink { - - @Bean - public Consumer sink() { - return System.out::println; - } - } -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooBindingProperties.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooBindingProperties.java deleted file mode 100644 index 146a11cd0..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooBindingProperties.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.utils; - -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * @author Soby Chacko - */ -public class FooBindingProperties implements BinderSpecificPropertiesProvider { - - private FooProducerProperties producer = new FooProducerProperties(); - - private FooConsumerProperties consumer = new FooConsumerProperties(); - - public FooProducerProperties getProducer() { - return this.producer; - } - - public void setProducer(FooProducerProperties producer) { - this.producer = producer; - } - - public FooConsumerProperties getConsumer() { - return this.consumer; - } - - public void setConsumer(FooConsumerProperties consumer) { - this.consumer = consumer; - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooConsumerProperties.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooConsumerProperties.java deleted file mode 100644 index f1419d37e..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooConsumerProperties.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.utils; - -/** - * @author Soby Chacko - */ -public class FooConsumerProperties { - - String extendedProperty; - - public String getExtendedProperty() { - return this.extendedProperty; - } - - public void setExtendedProperty(String extendedProperty) { - this.extendedProperty = extendedProperty; - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooExtendedBindingProperties.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooExtendedBindingProperties.java deleted file mode 100644 index 0b8c81219..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooExtendedBindingProperties.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.utils; - -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.stream.binder.AbstractExtendedBindingProperties; -import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider; - -/** - * @author Soby Chacko - */ -@ConfigurationProperties("spring.cloud.stream.foo") -public class FooExtendedBindingProperties extends - AbstractExtendedBindingProperties { - - private static final String DEFAULTS_PREFIX = "spring.cloud.stream.foo.default"; - - @Override - public String getDefaultsPrefix() { - return DEFAULTS_PREFIX; - } - - public Map getBindings() { - return this.doGetBindings(); - } - - @Override - public Class getExtendedPropertiesEntryClass() { - return FooBindingProperties.class; - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooProducerProperties.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooProducerProperties.java deleted file mode 100644 index b69351bc3..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/FooProducerProperties.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.utils; - -/** - * @author Soby Chacko - */ -public class FooProducerProperties { - - String extendedProperty; - - public String getExtendedProperty() { - return this.extendedProperty; - } - - public void setExtendedProperty(String extendedProperty) { - this.extendedProperty = extendedProperty; - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/MockBinderConfiguration.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/MockBinderConfiguration.java deleted file mode 100644 index b013f2302..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/MockBinderConfiguration.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2015-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.cloud.stream.utils; - -import org.mockito.Mockito; - -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Marius Bogoevici - */ -@Configuration -public class MockBinderConfiguration { - - @Bean - public Binder binder() { - return Mockito.mock(Binder.class, - Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS)); - } - -} diff --git a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/MockExtendedBinderConfiguration.java b/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/MockExtendedBinderConfiguration.java deleted file mode 100644 index 5d73322b7..000000000 --- a/core/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/utils/MockExtendedBinderConfiguration.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2018-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.cloud.stream.utils; - -import java.util.HashMap; -import java.util.Map; - -import org.mockito.Mockito; - -import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.StandardEnvironment; - -import static org.mockito.Mockito.when; - -/** - * @author Soby Chacko - */ -@Configuration -public class MockExtendedBinderConfiguration { - - @SuppressWarnings("rawtypes") - @Bean - public Binder extendedPropertiesBinder() { - Binder mock = Mockito.mock(Binder.class, - Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS) - .extraInterfaces(ExtendedPropertiesBinder.class)); - ConfigurableEnvironment environment = new StandardEnvironment(); - Map propertiesToAdd = new HashMap<>(); - propertiesToAdd.put("spring.cloud.stream.foo.default.consumer.extendedProperty", - "someFancyExtension"); - propertiesToAdd.put("spring.cloud.stream.foo.default.producer.extendedProperty", - "someFancyExtension"); - environment.getPropertySources() - .addLast(new MapPropertySource("extPropertiesConfig", propertiesToAdd)); - - ConfigurableApplicationContext applicationContext = new GenericApplicationContext(); - applicationContext.setEnvironment(environment); - - FooExtendedBindingProperties fooExtendedBindingProperties = new FooExtendedBindingProperties(); - fooExtendedBindingProperties.setApplicationContext(applicationContext); - - final FooConsumerProperties fooConsumerProperties = fooExtendedBindingProperties - .getExtendedConsumerProperties("input"); - final FooProducerProperties fooProducerProperties = fooExtendedBindingProperties - .getExtendedProducerProperties("output"); - - when(((ExtendedPropertiesBinder) mock).getExtendedConsumerProperties("input")) - .thenReturn(fooConsumerProperties); - - when(((ExtendedPropertiesBinder) mock).getExtendedProducerProperties("output")) - .thenReturn(fooProducerProperties); - - return mock; - } - -} diff --git a/core/spring-cloud-stream/src/test/resources/META-INF/spring.binders b/core/spring-cloud-stream/src/test/resources/META-INF/spring.binders deleted file mode 100644 index b6e4a6729..000000000 --- a/core/spring-cloud-stream/src/test/resources/META-INF/spring.binders +++ /dev/null @@ -1,2 +0,0 @@ -mock:\ -org.springframework.cloud.stream.utils.MockBinderConfiguration diff --git a/core/spring-cloud-stream/src/test/resources/binder-aot-test/application.yml b/core/spring-cloud-stream/src/test/resources/binder-aot-test/application.yml deleted file mode 100644 index c85248c56..000000000 --- a/core/spring-cloud-stream/src/test/resources/binder-aot-test/application.yml +++ /dev/null @@ -1,21 +0,0 @@ -spring.cloud: - function: - definition: fooSource;fooSink - stream: - default-binder: mockBinder1 - binders: - mockBinder1: - type: mock - environment: - foo: bar1 - mockBinder2: - type: mock - environment: - foo: bar2 - bindings: - fooSource-out-0: - destination: fooSink-in-0 - binder: mockBinder2 - fooSink-in-0: - destination: fooSource-out-0 - binder: mockBinder2 diff --git a/core/spring-cloud-stream/src/test/resources/binder1/META-INF/spring.binders b/core/spring-cloud-stream/src/test/resources/binder1/META-INF/spring.binders deleted file mode 100644 index 1d4a3007d..000000000 --- a/core/spring-cloud-stream/src/test/resources/binder1/META-INF/spring.binders +++ /dev/null @@ -1 +0,0 @@ -binder1=org.springframework.cloud.stream.binder.stub1.StubBinder1Configuration diff --git a/core/spring-cloud-stream/src/test/resources/binder2/META-INF/spring.binders b/core/spring-cloud-stream/src/test/resources/binder2/META-INF/spring.binders deleted file mode 100644 index 26dc2bc5e..000000000 --- a/core/spring-cloud-stream/src/test/resources/binder2/META-INF/spring.binders +++ /dev/null @@ -1,3 +0,0 @@ -binder2:\ -org.springframework.cloud.stream.binder.stub2.StubBinder2ConfigurationA,\ -org.springframework.cloud.stream.binder.stub2.StubBinder2ConfigurationB diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/arbitrary-binding-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/arbitrary-binding-test.properties deleted file mode 100644 index c9e55e3e3..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/arbitrary-binding-test.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.cloud.stream.bindings.process1-in-0.destination=someQueue.0 -spring.cloud.stream.bindings.process1-out-0.destination=someQueue.1 -spring.cloud.stream.bindings.process2-in-0.destination=someQueue.2 -spring.cloud.stream.bindings.process2-out-0.destination=someQueue.3 diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/cloud-function-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/cloud-function-test.properties deleted file mode 100644 index 9c3e4cf55..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/cloud-function-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -spring.cloud.stream.function.producerProperties.useNativeEncoding:true -spring.cloud.stream.function.consumerProperties.maxAttempts:5 diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/custom-partitioned-producer-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/custom-partitioned-producer-test.properties deleted file mode 100644 index a96bf5076..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/custom-partitioned-producer-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -spring.cloud.stream.bindings.output.destination=partOut -spring.cloud.stream.bindings.output.producer.partitionCount=3 diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/partitioned-consumer-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/partitioned-consumer-test.properties deleted file mode 100644 index d25e45572..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/partitioned-consumer-test.properties +++ /dev/null @@ -1,5 +0,0 @@ -spring.cloud.stream.bindings.sink-in-0.destination=partIn -spring.cloud.stream.bindings.sink-in-0.consumer.partitioned=true -spring.cloud.stream.instanceCount=2 -spring.cloud.stream.instanceIndex=0 - diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/partitioned-producer-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/partitioned-producer-test.properties deleted file mode 100644 index aee346650..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/partitioned-producer-test.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.cloud.stream.bindings.output.destination=partOut -spring.cloud.stream.bindings.output.producer.partitionKeyExpression=payload -spring.cloud.stream.bindings.output.producer.partitionCount=3 - diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/processor-binding-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/processor-binding-test.properties deleted file mode 100644 index 06b42431a..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/processor-binding-test.properties +++ /dev/null @@ -1,2 +0,0 @@ -spring.cloud.stream.bindings.input.destination=testtock.0 -spring.cloud.stream.bindings.output.destination=testtock.1 diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/sink-binding-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/sink-binding-test.properties deleted file mode 100644 index 9d96ac395..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/sink-binding-test.properties +++ /dev/null @@ -1 +0,0 @@ -spring.cloud.stream.bindings.input.destination=testtock diff --git a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/source-binding-test.properties b/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/source-binding-test.properties deleted file mode 100644 index a9a6733d0..000000000 --- a/core/spring-cloud-stream/src/test/resources/org/springframework/cloud/stream/binder/source-binding-test.properties +++ /dev/null @@ -1 +0,0 @@ -spring.cloud.stream.bindings.output.destination=testtock diff --git a/core/spring-cloud-stream/work.txt b/core/spring-cloud-stream/work.txt deleted file mode 100644 index 8fb75740b..000000000 --- a/core/spring-cloud-stream/work.txt +++ /dev/null @@ -1 +0,0 @@ -- MetersPublisherBinding uses Output. Need to rework \ No newline at end of file diff --git a/docs/.github/workflows/deploy-docs.yml b/docs/.github/workflows/deploy-docs.yml deleted file mode 100644 index be4b92dfc..000000000 --- a/docs/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Deploy Docs -on: - push: - branches-ignore: [ gh-pages ] - tags: '**' - repository_dispatch: - types: request-build-reference # legacy - #schedule: - #- cron: '0 10 * * *' # Once per day at 10am UTC - workflow_dispatch: -permissions: - actions: write -jobs: - build: - runs-on: ubuntu-latest - # if: github.repository_owner == 'spring-cloud' - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: docs-build - fetch-depth: 1 - - name: Dispatch (partial build) - if: github.ref_type == 'branch' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} - - name: Dispatch (full build) - if: github.ref_type == 'tag' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) diff --git a/docs/.jdk8 b/docs/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/antora.yml b/docs/antora.yml deleted file mode 100644 index 15b346da0..000000000 --- a/docs/antora.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: PROJECT_WITHOUT_SPRING -version: true -title: PROJECT_NAME -nav: - - modules/ROOT/nav.adoc -ext: - collector: - run: - command: ./mvnw --no-transfer-progress -B process-resources -Pdocs -pl docs -Dantora-maven-plugin.phase=none -Dgenerate-docs.phase=none -Dgenerate-readme.phase=none -Dgenerate-cloud-resources.phase=none -Dmaven-dependency-plugin-for-docs.phase=none -Dmaven-dependency-plugin-for-docs-classes.phase=none -DskipTests - local: true - scan: - dir: ./target/classes/antora-resources/ diff --git a/docs/modules/ROOT/assets/images/SCSt-groups.png b/docs/modules/ROOT/assets/images/SCSt-groups.png deleted file mode 100644 index 931ac9727..000000000 Binary files a/docs/modules/ROOT/assets/images/SCSt-groups.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/SCSt-overview.png b/docs/modules/ROOT/assets/images/SCSt-overview.png deleted file mode 100644 index 6cfd685a3..000000000 Binary files a/docs/modules/ROOT/assets/images/SCSt-overview.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/SCSt-partitioning.png b/docs/modules/ROOT/assets/images/SCSt-partitioning.png deleted file mode 100644 index 833d2ac66..000000000 Binary files a/docs/modules/ROOT/assets/images/SCSt-partitioning.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/SCSt-sensors.png b/docs/modules/ROOT/assets/images/SCSt-sensors.png deleted file mode 100644 index 6a15c8729..000000000 Binary files a/docs/modules/ROOT/assets/images/SCSt-sensors.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/SCSt-with-binder.png b/docs/modules/ROOT/assets/images/SCSt-with-binder.png deleted file mode 100644 index b4d66fd5e..000000000 Binary files a/docs/modules/ROOT/assets/images/SCSt-with-binder.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/custom_vs_global_error_channels.png b/docs/modules/ROOT/assets/images/custom_vs_global_error_channels.png deleted file mode 100644 index 6363d5162..000000000 Binary files a/docs/modules/ROOT/assets/images/custom_vs_global_error_channels.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/producers-consumers.png b/docs/modules/ROOT/assets/images/producers-consumers.png deleted file mode 100755 index 9990897dd..000000000 Binary files a/docs/modules/ROOT/assets/images/producers-consumers.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/redis-binder.png b/docs/modules/ROOT/assets/images/redis-binder.png deleted file mode 100755 index 1832bd2e3..000000000 Binary files a/docs/modules/ROOT/assets/images/redis-binder.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/registration.png b/docs/modules/ROOT/assets/images/registration.png deleted file mode 100644 index d2c044c1e..000000000 Binary files a/docs/modules/ROOT/assets/images/registration.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/schema_reading.png b/docs/modules/ROOT/assets/images/schema_reading.png deleted file mode 100644 index df9985630..000000000 Binary files a/docs/modules/ROOT/assets/images/schema_reading.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/schema_resolution.png b/docs/modules/ROOT/assets/images/schema_resolution.png deleted file mode 100644 index 2acbfac78..000000000 Binary files a/docs/modules/ROOT/assets/images/schema_resolution.png and /dev/null differ diff --git a/docs/modules/ROOT/assets/images/spring-initializr.png b/docs/modules/ROOT/assets/images/spring-initializr.png deleted file mode 100644 index bcb6ae20d..000000000 Binary files a/docs/modules/ROOT/assets/images/spring-initializr.png and /dev/null differ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc deleted file mode 100644 index 570cbbcf8..000000000 --- a/docs/modules/ROOT/nav.adoc +++ /dev/null @@ -1,100 +0,0 @@ -* xref:index.adoc[] -* xref:spring-cloud-stream.adoc[] -** xref:spring-cloud-stream/overview-application-model.adoc[] -** xref:spring-cloud-stream/overview-binder-abstraction.adoc[] -** xref:spring-cloud-stream/overview-persistent-publish-subscribe-support.adoc[] -** xref:spring-cloud-stream/consumer-groups.adoc[] -** xref:spring-cloud-stream/consumer-types.adoc[] -** xref:spring-cloud-stream/partitioning.adoc[] -** xref:spring-cloud-stream/destination-binders.adoc[] -** xref:spring-cloud-stream/bindings.adoc[] -** xref:spring-cloud-stream/producing-and-consuming-messages.adoc[] -** xref:spring-cloud-stream/event-routing.adoc[] -** xref:spring-cloud-stream/post-processing-after-sending-message.adoc[] -** xref:spring-cloud-stream/overview-error-handling.adoc[] -** xref:spring-cloud-stream/producers-and-consumers.adoc[] -** xref:spring-cloud-stream/overview-binder-api.adoc[] -** xref:spring-cloud-stream/binder-detection.adoc[] -** xref:spring-cloud-stream/multiple-binders.adoc[] -** xref:spring-cloud-stream/multiple-systems.adoc[] -** xref:spring-cloud-stream/binder-customizer.adoc[] -** xref:spring-cloud-stream/binding_visualization_control.adoc[] -** xref:spring-cloud-stream/binder-configuration-properties.adoc[] -** xref:spring-cloud-stream/overview-custom-binder-impl.adoc[] -** xref:spring-cloud-stream/binding-service-properties.adoc[] -** xref:spring-cloud-stream/binding-properties.adoc[] -** xref:spring-cloud-stream/mechanics.adoc[] -** xref:spring-cloud-stream/provided-messageconverters.adoc[] -** xref:spring-cloud-stream/overview-user-defined-message-converters.adoc[] -** xref:spring-cloud-stream/overview-connecting-multiple-application-instances.adoc[] -** xref:spring-cloud-stream/overview-instance-index-instance-count.adoc[] -** xref:spring-cloud-stream/overview-partitioning.adoc[] -** xref:spring-cloud-stream/spring_integration_test_binder.adoc[] -** xref:spring-cloud-stream/deploying-applications-on-cloudfoundry.adoc[] -* xref:preface.adoc[] -* xref:binders.adoc[] -* xref:README.adoc[] -* xref:_configprops.adoc[] -* xref:intro.adoc[] -** xref:kafka/kafka-reactive.adoc[] -** xref:kafka/kafka-streams.adoc[] -*** xref:kafka/streams/usage.adoc[] -*** xref:kafka/streams/overview.adoc[] -*** xref:kafka/streams/programming-model.adoc[] -*** xref:kafka/streams/ancillaries-to-the-programming-model.adoc[] -*** xref:kafka/streams/record-serialization-and-deserialization.adoc[] -*** xref:kafka/streams/error-handling.adoc[] -*** xref:kafka/streams/retrying-critical-business-logic.adoc[] -*** xref:kafka/streams/state-store.adoc[] -*** xref:kafka/streams/interactive-queries.adoc[] -*** xref:kafka/streams/health-indicator.adoc[] -*** xref:kafka/streams/accessing-metrics.adoc[] -*** xref:kafka/streams/mixing-high-level-dsl-and-low-level-processor-api.adoc[] -*** xref:kafka/streams/partition-support-on-the-outbound.adoc[] -*** xref:kafka/streams/streamsbuilderfactorybean-customizer.adoc[] -*** xref:kafka/streams/timestamp-extractor.adoc[] -*** xref:kafka/streams/multi-binders-with-based-binders-and-regular-binder.adoc[] -*** xref:kafka/streams/state-cleanup.adoc[] -*** xref:kafka/streams/topology-visualization.adoc[] -*** xref:kafka/streams/event-type-based-routing-in-applications.adoc[] -*** xref:kafka/streams/binding-visualization-and-control-in-binder.adoc[] -*** xref:kafka/streams/manually-starting-processors.adoc[] -*** xref:kafka/streams/manually-starting-processors-selectively.adoc[] -*** xref:kafka/streams/tracing-using-spring-cloud-sleuth.adoc[] -*** xref:kafka/streams/configuration-options.adoc[] -** xref:kafka/kafka_dlq.adoc[] -** xref:kafka/kafka_overview.adoc[] -*** xref:kafka/kafka_overview/usage.adoc[] -*** xref:kafka/kafka_overview/overview.adoc[] -*** xref:kafka/kafka_overview/configuration-options.adoc[] -*** xref:kafka/kafka_overview/transactional-binder.adoc[] -*** xref:kafka/kafka_overview/error-channels.adoc[] -*** xref:kafka/kafka_overview/metrics.adoc[] -*** xref:kafka/kafka_overview/tombstones.adoc[] -*** xref:kafka/kafka_overview/rebalance-listener.adoc[] -*** xref:kafka/kafka_overview/retry-and-dlq-processing.adoc[] -*** xref:kafka/kafka_overview/consumer-producer-config-customizer.adoc[] -*** xref:kafka/kafka_overview/admin-client-config-customization.adoc[] -*** xref:kafka/kafka_overview/custom-binder-health-indicator.adoc[] -*** xref:kafka/kafka_overview/custom-binder-health-indicator-example.adoc[] -** xref:kafka/kafka_partitions.adoc[] -** xref:kafka/kafka_tips.adoc[] -** xref:kafka/spring-cloud-stream-binder-kafka.adoc[] -** xref:pulsar/pulsar_binder.adoc[] -** xref:pulsar/spring-cloud-stream-binder-pulsar.adoc[] -** xref:rabbit/rabbit_dlq.adoc[] -** xref:rabbit/rabbit_overview.adoc[] -*** xref:rabbit/rabbit_overview/binder-properties.adoc[] -*** xref:rabbit/rabbit_overview/rabbitmq-consumer-properties.adoc[] -*** xref:rabbit/rabbit_overview/rabbitmq-stream-consumer.adoc[] -*** xref:rabbit/rabbit_overview/advanced-listener-container-configuration.adoc[] -***** xref:rabbit/rabbit_overview/advanced-queue/exchange/binding-configuration.adoc[] -*** xref:rabbit/rabbit_overview/receiving-batch.adoc[] -*** xref:rabbit/rabbit_overview/prod-props.adoc[] -*** xref:rabbit/rabbit_overview/publisher-confirms.adoc[] -*** xref:rabbit/rabbit_overview/rabbitmq-stream-producer.adoc[] -*** xref:rabbit/rabbit_overview/putting-it-all-together.adoc[] -** xref:rabbit/rabbit_partitions.adoc[] -** xref:rabbit/spring-cloud-stream-binder-rabbit.adoc[] -* xref:sagan-index.adoc[] -** xref:schema-registry/spring-cloud-stream-schema-registry.adoc[] diff --git a/docs/modules/ROOT/pages/README.adoc b/docs/modules/ROOT/pages/README.adoc deleted file mode 100644 index 8c06a10ef..000000000 --- a/docs/modules/ROOT/pages/README.adoc +++ /dev/null @@ -1,30 +0,0 @@ -:jdkversion: 17 -:github-tag: master -:github-repo: spring-cloud/spring-cloud-stream - -:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} -:github-code: https://github.com/{github-repo}/tree/{github-tag} - -image::https://circleci.com/gh/spring-cloud/spring-cloud-stream.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-stream"] -image::https://codecov.io/gh/spring-cloud/spring-cloud-stream/branch/{github-tag}/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-stream"] - -// ====================================================================================== - -[[introduction]] -= Introduction - - -[[resources]] -= Resources - -For more information, please visit the https://spring.io/projects/spring-cloud-stream[project website]: - -[[building]] -= Building - -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/building.adoc[] - -[[contributing]] -= Contributing - -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] diff --git a/docs/modules/ROOT/pages/_configprops.adoc b/docs/modules/ROOT/pages/_configprops.adoc deleted file mode 100644 index 040af820a..000000000 --- a/docs/modules/ROOT/pages/_configprops.adoc +++ /dev/null @@ -1,22 +0,0 @@ -|=== -|Name | Default | Description - -|spring.cloud.stream.binders | | Additional per-binder properties (see {@link BinderProperties}) if more then one binder of the same type is used (i.e., connect to multiple instances of RabbitMq). Here you can specify multiple binder configurations, each with different environment settings. For example; spring.cloud.stream.binders.rabbit1.environment. . . , spring.cloud.stream.binders.rabbit2.environment. . . -|spring.cloud.stream.binding-retry-interval | `+++30+++` | Retry interval (in seconds) used to schedule binding attempts. Default: 30 sec. -|spring.cloud.stream.bindings | | Additional binding properties (see {@link BinderProperties}) per binding name (e.g., 'input`). For example; This sets the content-type for the 'input' binding of a Sink application: 'spring.cloud.stream.bindings.input.contentType=text/plain' -|spring.cloud.stream.default-binder | | The name of the binder to use by all bindings in the event multiple binders available (e.g., 'rabbit'). -|spring.cloud.stream.dynamic-destination-cache-size | `+++10+++` | The maximum size of Least Recently Used (LRU) cache of dynamic destinations. Once this size is reached, new destinations will trigger the removal of old destinations. Default: 10 -|spring.cloud.stream.dynamic-destinations | `+++[]+++` | A list of destinations that can be bound dynamically. If set, only listed destinations can be bound. -|spring.cloud.stream.function.bindings | | -|spring.cloud.stream.input-bindings | | A semi-colon delimited string to explicitly define input bindings (specifically for cases when there is no implicit trigger to create such bindings such as Function, Supplier or Consumer). -|spring.cloud.stream.instance-count | `+++1+++` | The number of deployed instances of an application. Default: 1. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-count" where 'foo' is the name of the binding. -|spring.cloud.stream.instance-index | `+++0+++` | The instance id of the application: a number from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index" where 'foo' is the name of the binding. -|spring.cloud.stream.instance-index-list | | A list of instance id's from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index-list" where 'foo' is the name of the binding. This setting will override the one set in 'spring.cloud.stream.instance-index' -|spring.cloud.stream.integration.message-handler-not-propagated-headers | | Message header names that will NOT be copied from the inbound message. -|spring.cloud.stream.output-bindings | | A semi-colon delimited string to explicitly define output bindings (specifically for cases when there is no implicit trigger to create such bindings such as Function, Supplier or Consumer). -|spring.cloud.stream.override-cloud-connectors | `+++false+++` | This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). The typical usage of this property is to be nested in a customized environment when connecting to multiple systems. -|spring.cloud.stream.pollable-source | `+++none+++` | A semi-colon delimited list of binding names of pollable sources. Binding names follow the same naming convention as functions. For example, name '...pollable-source=foobar' will be accessible as 'foobar-iin-0'' binding -|spring.cloud.stream.sendto.destination | `+++none+++` | The name of the header used to determine the name of the output destination -|spring.cloud.stream.source | | A semi-colon delimited string representing the names of the sources based on which source bindings will be created. This is primarily to support cases where source binding may be required without providing a corresponding Supplier. (e.g., for cases where the actual source of data is outside of scope of spring-cloud-stream - HTTP -> Stream) @deprecated use {@link #outputBindings} - -|=== diff --git a/docs/modules/ROOT/pages/binders.adoc b/docs/modules/ROOT/pages/binders.adoc deleted file mode 100644 index 27dae96c1..000000000 --- a/docs/modules/ROOT/pages/binders.adoc +++ /dev/null @@ -1,16 +0,0 @@ -*{project-version}* - -[[binders]] -= Binder Implementations -:page-section-summary-toc: 1 - -The following binder implementations are available: - -* https://cloud.spring.io/spring-cloud-stream-binder-rabbit/[RabbitMQ] -* https://cloud.spring.io/spring-cloud-stream-binder-kafka/[Apache Kafka] -* https://github.com/spring-cloud/spring-cloud-stream-binder-aws-kinesis[Amazon Kinesis] -* https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-pubsub-stream-binder[Google PubSub _(partner maintained)_] -* https://github.com/SolaceProducts/solace-spring-cloud/tree/master/solace-spring-cloud-starters/solace-spring-cloud-stream-starter#spring-cloud-stream-binder-for-solace-pubsub[Solace PubSub+ _(partner maintained)_] -* https://aka.ms/spring/docs#spring-cloud-stream-binder-for-azure-event-hubs[Azure Event Hubs _(partner maintained)_] -* https://aka.ms/spring/docs#spring-cloud-stream-binder-for-azure-service-bus[Azure Service Bus Binder _(partner maintained)_] -* https://github.com/alibaba/spring-cloud-alibaba/wiki/RocketMQ-en[Apache RocketMQ _(partner maintained)_] diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc deleted file mode 100644 index 8cc88a560..000000000 --- a/docs/modules/ROOT/pages/index.adoc +++ /dev/null @@ -1,30 +0,0 @@ -[[spring-cloud-stream-reference-documentation]] -= Spring Cloud Stream Reference Documentation -Sabby Anandan; Marius Bogoevici; Eric Bottard; Mark Fisher; Ilayaperumal Gopinathan; Mark Heckler; Gunnar Hillert; Mark Pollack; Patrick Peralta; Glenn Renfro; Thomas Risberg; Dave Syer; David Turanski; Janne Valkealahti; Benjamin Klein; Vinicius Carvalho; Gary Russell; Oleg Zhurakousky; Jay Bryant; Soby Chacko; Domenico Sibilio - -*{project-version}* - -:docinfo: shared - -The reference documentation consists of the following sections: - -[horizontal] -xref:spring-cloud-stream.adoc#spring-cloud-stream-reference[Overview] :: History, Quick Start, Concepts, Architecture Overview, Binder Abstraction, and Core Features - -xref:rabbit/spring-cloud-stream-binder-rabbit.adoc[Rabbit MQ Binder] :: Spring Cloud Stream binder reference for Rabbit MQ -<> :: Spring Cloud Stream binder reference for Apache Kafka -<> :: Spring Cloud Stream binder reference for Apache Kafka Streams -xref:pulsar/spring-cloud-stream-binder-pulsar.adoc[Apache Pulsar Binder] :: Spring Cloud Stream binder reference for Apache Pulsar -https://github.com/SolaceProducts/solace-spring-cloud/tree/master/solace-spring-cloud-starters/solace-spring-cloud-stream-starter#spring-cloud-stream-binder-for-solace-pubsub[Solace PubSub+ Binder] :: Spring Cloud Stream binder reference for Solace PubSub+ (Partner Maintained) - -xref:spring-cloud-stream/overview-error-handling.adoc#spring-cloud-stream-overview-binders[Additional Binders] :: A collection of Partner maintained binder implementations for Spring Cloud Stream (e.g., Azure Event Hubs, Google PubSub, Solace PubSub+) -xref:schema-registry/spring-cloud-stream-schema-registry.adoc[Schema Registry] :: Spring Cloud Stream Schema Registry Reference -https://github.com/spring-cloud/spring-cloud-stream-samples/[Spring Cloud Stream Samples] :: A curated collection of repeatable Spring Cloud Stream samples to walk through the features - -Relevant Links: - -[horizontal] -https://cloud.spring.io/spring-cloud-dataflow/[Spring Cloud Data Flow] :: Spring Cloud Data Flow -http://www.enterpriseintegrationpatterns.com/[Enterprise Integration Patterns] :: Patterns and Best Practices for Enterprise Integration -https://spring.io/projects/spring-integration[Spring Integration] :: Spring Integration framework - diff --git a/docs/modules/ROOT/pages/intro.adoc b/docs/modules/ROOT/pages/intro.adoc deleted file mode 100644 index 5c0d10a16..000000000 --- a/docs/modules/ROOT/pages/intro.adoc +++ /dev/null @@ -1,75 +0,0 @@ -Spring Cloud Stream is a framework for building message-driven microservice applications. -Spring Cloud Stream builds upon Spring Boot to create standalone, production-grade Spring applications and uses Spring Integration to provide connectivity to message brokers. -It provides opinionated configuration of middleware from several vendors, introducing the concepts of persistent publish-subscribe semantics, consumer groups, and partitions. -These are called binder implementations in the parlance of Spring Cloud Stream. -Out of the box, Spring Cloud Stream provides binder implementations for Apache Kafka and RabbitMQ. -While these two binder implementations are based on Message Channels, Spring Cloud Stream also provides another binder implementation for Kafka Streams that does not use message channels, but native Kafka Streams types such as KStream, KTable etc. - -Below, you can find more information on how to use these various out-of-the-box binder implementations in your applications. - -[[apache-kafka-binder]] -= Apache Kafka Binder - -[[usage]] -== Usage - -To use Apache Kafka binder, you need to add `spring-cloud-stream-binder-kafka` as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-kafka - ----- - -Alternatively, you can also use the Spring Cloud Stream Kafka Starter, as shown in the following example for Maven: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-stream-kafka - ----- - -[[apache-kafka-streams-binder]] -= Apache Kafka Streams Binder - -[[usage]] -== Usage - -To use Apache Kafka Streams binder, you need to add `spring-cloud-stream-binder-kafka-streams` as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-kafka-streams - ----- - -[[rabbitmq-binder]] -= RabbitMQ Binder -[[usage]] -== Usage - -To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-rabbit - ----- - -Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-stream-rabbit - ----- diff --git a/docs/modules/ROOT/pages/kafka/kafka-reactive.adoc b/docs/modules/ROOT/pages/kafka/kafka-reactive.adoc deleted file mode 100644 index 2a8e88cd3..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka-reactive.adoc +++ /dev/null @@ -1,237 +0,0 @@ -[[reactive-kafka-binder]] -= Reactive Kafka Binder - -Kafka binder in Spring Cloud Stream provides a dedicated reactive binder based on the https://projectreactor.io/docs/kafka/release/reference/[Reactor Kafka] project. -This reactive Kafka binder enables full end-to-end reactive capabilities such as backpressure, reactive streams etc. in applications based on Apache Kafka. -When your Spring Cloud Stream Kafka application is written using reactive types (`Flux`, `Mono` etc.), it is recommended to use this reactive Kafka binder instead of the regular message channel based Kafka binder. - -[[maven-coordinates]] -== Maven Coordinates - -Following are the maven coordinates for the reactive Kafka binder. - -``` - - org.springframework.cloud - spring-cloud-stream-binder-kafka-reactive - -``` - -[[basic-example-using-the-reactive-kafka-binder]] -== Basic Example using the Reactive Kafka Binder - -In this section, we show some basic code snippets for writing a reactive Kafka application using the reactive binder and details around them. - -==== -[source, java] ----- -@Bean -public Function, Flux> uppercase() { - return s -> s.map(String::toUpperCase); -} ----- -==== - -You can use the above `upppercase` function with both message channel based Kafka binder (`spring-cloud-stream-binder-kafka`) as well as the reactive Kafka binder (`spring-cloud-stream-binder-kafka-reactive`), the topic of discussion in this section. -When using this function with the regular Kafka binder, although you are using reactive types in the application (i.e., in the `uppercase` function), you only get the reactive streams within the execution of your function. -Outside the function's execution context, there is no reactive benefits since the underlying binder is not based on the reactive stack. -Therefore, although this might look like it is bringing a full end-to-end reactive stack, this application is only partially reactive. - -Now assume that you are using the proper reactive binder for Kafka - `spring-cloud-stream-binder-kafka-reactive` with the above function's application. -This binder implementation will give the full reactive benefits all the way from consumption on the top end to publishing at the bottom end of the chain. -This is because the underlying binder is built on top of https://projectreactor.io/docs/kafka/release/reference/[Reactor Kafka]'s core API's. -On the consumer side, it makes use of the https://projectreactor.io/docs/kafka/release/reference/#api-guide-receiver[KafkaReceiver] which is a reactive implementation of a Kafka consumer. -Similarly, on the producer side, it uses https://projectreactor.io/docs/kafka/release/reference/#api-guide-sender[KafkaSender] API which is the reactive implementation of a Kafka producer. -Since the foundations of the reactive Kafka binder is built upon a proper reactive Kafka API, applications get the full benefits of using reactive technologies. -Things like automatic back pressure, among other reactive capabilities, are built-in for the application when using this reactive Kafka binder. - -Starting with version 4.0.2, you can customize the `ReceiverOptions` and `SenderOptions` by providing one or more `ReceiverOptionsCustomizer` or `SenderOptionsCustomizer` beans respectively. -They are `BiFunction` s which receive the binding name and initial options, returning the customized options. -The interfaces extend `Ordered` so the customizers will be applied in the order required, when more than one are present. - -IMPORTANT: The binder does not commit offsets by default. -Starting with version 4.0.2, the `KafkaHeaders.ACKNOWLEDGMENT` header contains a `ReceiverOffset` object which allows you to cause the offset to be committed by calling its `acknowledge()` or `commit()` methods. - -==== -[source, java] ----- -@Bean -public Consumer> consume() { - return msg -> { - process(msg.getPayload()); - msg.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, ReceiverOffset.class).acknowledge(); - } -} ----- -==== - -Refer to the `reactor-kafka` documentation and javadocs for more information. - -In addition, starting with version 4.0.3, the Kafka consumer property `reactiveAtmostOnce` can be set to `true` and the binder will automatically commit the offsets before records returned by each poll are processed. -Also, starting with version 4.0.3, you can set the consumer property `reactiveAutoCommit` to `true` and the the binder will automatically commit the offsets after the records returned by each poll are processed. -In these cases, the acknowledgment header is not present. - -IMPORTANT: 4.0.2 also provided `reactiveAutoCommit`, but the implementation was incorrect, it behaved similarly to `reactiveAtMostOnce`. - -The following is an example of how to use `reaciveAutoCommit`. - -==== -[source, java] ----- -@Bean -Consumer>>> input() { - return flux -> flux - .doOnNext(inner -> inner - .doOnNext(val -> { - log.info(val.value()); - }) - .subscribe()) - .subscribe(); -} ----- -==== - -Note that `reactor-kafka` returns a `Flux>>` when using auto commit. -Given that Spring has no access to the contents of the inner flux, the application must deal with the native `ConsumerRecord`; there is no message conversion or conversion service applied to the contents. -This requires the use of native decoding (by specifying a `Deserializer` of the appropriate type in the configuration) to return record keys/values of the desired types. - -[[consuming-records-in-the-raw-format]] -== Consuming Records in the Raw Format - -In the above `upppercase` function, we are consuming the record as `Flux` and then produce it as `Flux`. -There might be occasions in which you need to receive the record in the original received format - the `ReceiverRecord`. -Here is such a function. - -==== -[source, java] ----- -@Bean -public Function>, Flux> lowercase() { - return s -> s.map(rec -> new String(rec.value()).toLowerCase()); -} ----- -==== - -In this function, note that, we are consuming the record as `Flux>` and then producing it as `Flux`. -`ReceiverRecord` is the basic received record which is a specialized Kafka `ConsumerRecord` in Reactor Kafka. -When using the reactive Kafka binder, the above function will give you access to the `ReceiverRecord` type for each incoming record. -However, in this case, you need to provide a custom implementation for a https://docs.spring.io/spring-kafka/docs/current/api/org/springframework/kafka/support/converter/RecordMessageConverter.html[RecordMessageConverter]. -By default, the reactive Kafka binder uses a https://docs.spring.io/spring-kafka/docs/current/api/org/springframework/kafka/support/converter/MessagingMessageConverter.html[MessagingMessageConverter] that converts the payload and headers from the `ConsumerRecord`. -Therefore, by the time your handler method receives it, the payload is already extracted from the received record and passed onto the method as in the case of the first function we looked above. -By providing a custom `RecordMessageConverter` implementation in the application, you can override the default behavior. -For example, if you want to consume the record as raw `Flux>`, then you can provide the following bean definition in the application. - -==== -[source, java] ----- -@Bean -RecordMessageConverter fullRawReceivedRecord() { - return new RecordMessageConverter() { - - private final RecordMessageConverter converter = new MessagingMessageConverter(); - - @Override - public Message toMessage(ConsumerRecord record, Acknowledgment acknowledgment, - Consumer consumer, Type payloadType) { - return MessageBuilder.withPayload(record).build(); - } - - @Override - public ProducerRecord fromMessage(Message message, String defaultTopic) { - return this.converter.fromMessage(message, defaultTopic); - } - - }; -} ----- -==== - -Then, you need to instruct the framework to use this converter for the required binding. -Here is an example based on our `lowercase` function. - -``` -spring.cloud.stream.kafka.bindings.lowercase-in-0.consumer.converterBeanName=fullRawReceivedRecord" -``` - -`lowercase-in-0` is the input binding name for our `lowercase` function. -For the outbound (`lowecase-out-0`), we still use the regular `MessagingMessageConverter`. - -In the `toMessage` implementation above, we receive the raw `ConsumerRecord` (`ReceiverRecord` since we are in a reactive binder context) and then wrap it inside a `Message`. -Then that message payload which is the `ReceiverRecord` is provided to the user method. - -If `reactiveAutoCommit` is `false` (default), call `rec.receiverOffset().acknowledge()` (or `commit()`) to cause the offset to be committed; if `reactiveAutoCommit` is `true`, the flux supplies `ConsumerRecord` s instead. -Refer to the `reactor-kafka` documentation and javadocs for more information. - -[[concurrency]] -== Concurrency - -When using reactive functions with the reactive Kafka binder, if you set concurrency on the consumer binding, then the binder creates as many dedicated `KafkaReceiver` objects as provided by the concurrency value. -In other words, this creates multiple reactive streams with separate `Flux` implementations. -This could be useful when you are consuming records from a partitioned topic. - -For example, assume that the incoming topic has at least three partitions. -Then you can set the following property. - -``` -spring.cloud.stream.bindings.lowercase-in-0.consumer.concurrency=3 -``` - -That will create three dedicated `KafkaReceiver` objects that generate three separate `Flux` implementations and then stream them to the handler method. - -[[multiplex]] -== Multiplex - -Starting with version 4.0.3, the common consumer property `multiplex` is now supported by the reactive binder, where a single binding can consume from multiple topics. -When `false` (default), a separate binding is created for each topic specified in a comma-delimited list in the common `destination` property. - -[[destination-is-pattern]] -== Destination is Pattern - -Starting with version 4.0.3, the `destination-is-pattern` Kafka binding consumer property is now supported. -The receiver options are conigured with a regex `Pattern`, allowing the binding to consume from any topic that matches the pattern. - -[[sender-result-channel]] -== Sender Result Channel - -Starting with version 4.0.3, you can configure the `resultMetadataChannel` to receive `SenderResult` s to determine success/failure of sends. - -The `SenderResult` contains `correlationMetadata` to allow you to correlate results with sends; it also contains `RecordMetadata`, which indicates the `TopicPartition` and offset of the sent record. - -The `resultMetadataChannel` **must** be a `FluxMessageChannel` instance. - -Here is an example of how to use this feature, with correlation metadata of type `Integer`: - -==== -[source, java] ----- -@Bean -FluxMessageChannel sendResults() { - return new FluxMessageChannel(); -} - -@ServiceActivator(inputChannel = "sendResults") -void handleResults(SenderResult result) { - if (result.exception() != null) { - failureFor(result); - } - else { - successFor(result); - } -} ----- -==== - -To set the correlation metadata on an output record, set the `CORRELATION_ID` header: - -==== -[source, java] ----- -streamBridge.send("words1", MessageBuilder.withPayload("foobar") - .setCorrelationId(42) - .build()); ----- -==== - -When using the feature with a `Function`, the function output type must be a `Message` with the correlation id header set to the desired value. - -Metadata should be unique, at least for the duration of the send. diff --git a/docs/modules/ROOT/pages/kafka/kafka-streams.adoc b/docs/modules/ROOT/pages/kafka/kafka-streams.adoc deleted file mode 100644 index 0338bceaa..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka-streams.adoc +++ /dev/null @@ -1,5 +0,0 @@ -[[kafka-streams-binder]] -= Kafka Streams Binder -:page-section-summary-toc: 1 - - diff --git a/docs/modules/ROOT/pages/kafka/kafka_dlq.adoc b/docs/modules/ROOT/pages/kafka/kafka_dlq.adoc deleted file mode 100644 index 50e3dd197..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_dlq.adoc +++ /dev/null @@ -1,150 +0,0 @@ -[[kafka-dlq-processing]] -= Dead-Letter Topic Processing - -[[dlq-partition-selection]] -== Dead-Letter Topic Partition Selection - -By default, records are published to the Dead-Letter topic using the same partition as the original record. -This means the Dead-Letter topic must have at least as many partitions as the original record. - -To change this behavior, add a `DlqPartitionFunction` implementation as a `@Bean` to the application context. -Only one such bean can be present. -The function is provided with the consumer group, the failed `ConsumerRecord` and the exception. -For example, if you always want to route to partition 0, you might use: - -==== -[source, java] ----- -@Bean -public DlqPartitionFunction partitionFunction() { - return (group, record, ex) -> 0; -} ----- -==== -NOTE: If you set a consumer binding's `dlqPartitions` property to 1 (and the binder's `minPartitionCount` is equal to `1`), there is no need to supply a `DlqPartitionFunction`; the framework will always use partition 0. -If you set a consumer binding's `dlqPartitions` property to a value greater than `1` (or the binder's `minPartitionCount` is greater than `1`), you **must** provide a `DlqPartitionFunction` bean, even if the partition count is the same as the original topic's. - -It is also possible to define a custom name for the DLQ topic. -In order to do so, create an implementation of `DlqDestinationResolver` as a `@Bean` to the application context. -When the binder detects such a bean, that takes precedence, otherwise it will use the `dlqName` property. -If neither of these are found, it will default to `error..`. -Here is an example of `DlqDestinationResolver` as a `@Bean`. - -==== -[source] ----- -@Bean -public DlqDestinationResolver dlqDestinationResolver() { - return (rec, ex) -> { - if (rec.topic().equals("word1")) { - return "topic1-dlq"; - } - else { - return "topic2-dlq"; - } - }; -} ----- -==== - -One important thing to keep in mind when providing an implementation for `DlqDestinationResolver` is that the provisioner in the binder will not auto create topics for the application. -This is because there is no way for the binder to infer the names of all the DLQ topics the implementation might send to. -Therefore, if you provide DLQ names using this strategy, it is the application's responsibility to ensure that those topics are created beforehand. - -[[dlq-handling]] -== Handling Records in a Dead-Letter Topic - -Because the framework cannot anticipate how users would want to dispose of dead-lettered messages, it does not provide any standard mechanism to handle them. -If the reason for the dead-lettering is transient, you may wish to route the messages back to the original topic. -However, if the problem is a permanent issue, that could cause an infinite loop. -The sample Spring Boot application within this topic is an example of how to route those messages back to the original topic, but it moves them to a "`parking lot`" topic after three attempts. -The application is another spring-cloud-stream application that reads from the dead-letter topic. -It exits when no messages are received for 5 seconds. - -The examples assume the original destination is `so8400out` and the consumer group is `so8400`. - -There are a couple of strategies to consider: - -* Consider running the rerouting only when the main application is not running. -Otherwise, the retries for transient errors are used up very quickly. -* Alternatively, use a two-stage approach: Use this application to route to a third topic and another to route from there back to the main topic. - -The following code listings show the sample application: - -.application.properties -[source] ----- -spring.cloud.stream.bindings.input.group=so8400replay -spring.cloud.stream.bindings.input.destination=error.so8400out.so8400 - -spring.cloud.stream.bindings.output.destination=so8400out - -spring.cloud.stream.bindings.parkingLot.destination=so8400in.parkingLot - -spring.cloud.stream.kafka.binder.configuration.auto.offset.reset=earliest - -spring.cloud.stream.kafka.binder.headers=x-retries ----- - -.Application -[source,java] ----- -@SpringBootApplication -public class ReRouteDlqKApplication implements CommandLineRunner { - - private static final String X_RETRIES_HEADER = "x-retries"; - - public static void main(String[] args) { - SpringApplication.run(ReRouteDlqKApplication.class, args).close(); - } - - private final AtomicInteger processed = new AtomicInteger(); - - @Autowired - private StreamBridge streamBridge; - - @Bean - public Function, Message> reRoute() { - return failed -> { - processed.incrementAndGet(); - Integer retries = failed.getHeaders().get(X_RETRIES_HEADER, Integer.class); - if (retries == null) { - System.out.println("First retry for " + failed); - return MessageBuilder.fromMessage(failed) - .setHeader(X_RETRIES_HEADER, 1) - .setHeader(BinderHeaders.PARTITION_OVERRIDE, - failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID)) - .build(); - } - else if (retries < 3) { - System.out.println("Another retry for " + failed); - return MessageBuilder.fromMessage(failed) - .setHeader(X_RETRIES_HEADER, retries + 1) - .setHeader(BinderHeaders.PARTITION_OVERRIDE, - failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID)) - .build(); - } - else { - System.out.println("Retries exhausted for " + failed); - streamBridge.send("parkingLot", MessageBuilder.fromMessage(failed) - .setHeader(BinderHeaders.PARTITION_OVERRIDE, - failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID)) - .build()); - } - return null; - }; - } - - @Override - public void run(String... args) throws Exception { - while (true) { - int count = this.processed.get(); - Thread.sleep(5000); - if (count == this.processed.get()) { - System.out.println("Idle, exiting"); - return; - } - } - } -} ----- diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview.adoc deleted file mode 100644 index 00ed476a6..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview.adoc +++ /dev/null @@ -1,11 +0,0 @@ -[partintro] --- -This guide describes the Apache Kafka implementation of the Spring Cloud Stream Binder. -It contains information about its design, usage, and configuration options, as well as information on how the Stream Cloud Stream concepts map onto Apache Kafka specific constructs. -In addition, this guide explains the Kafka Streams binding capabilities of Spring Cloud Stream. --- - -[[apache-kafka-binder]] -= Apache Kafka Binder -:page-section-summary-toc: 1 - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/admin-client-config-customization.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/admin-client-config-customization.adoc deleted file mode 100644 index f4fb3c7ba..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/admin-client-config-customization.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[admin-client-config-customization]] -= Customizing AdminClient Configuration -:page-section-summary-toc: 1 - -As with consumer and producer config customization above, applications can also customize the configuration for admin clients by providing an `AdminClientConfigCustomizer`. -AdminClientConfigCustomizer's configure method provides access to the admin client properties, using which you can define further customization. -Binder's Kafka topic provisioner gives the highest precedence for the properties given through this customizer. -Here is an example of providing this customizer bean. - -``` -@Bean -public AdminClientConfigCustomizer adminClientConfigCustomizer() { - return props -> { - props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL"); - }; -} -``` - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/configuration-options.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/configuration-options.adoc deleted file mode 100644 index 53d27cb72..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/configuration-options.adoc +++ /dev/null @@ -1,650 +0,0 @@ -[[configuration-options]] -= Configuration Options - -This section contains the configuration options used by the Apache Kafka binder. - -For common configuration options and properties pertaining to the binder, see the https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/current/reference/html/spring-cloud-stream.html#binding-properties[binding properties] in core documentation. - -[[kafka-binder-properties]] -== Kafka Binder Properties - -spring.cloud.stream.kafka.binder.brokers:: -A list of brokers to which the Kafka binder connects. -+ -Default: `localhost`. -spring.cloud.stream.kafka.binder.defaultBrokerPort:: -`brokers` allows hosts specified with or without port information (for example, `host1,host2:port2`). -This sets the default port when no port is configured in the broker list. -+ -Default: `9092`. -spring.cloud.stream.kafka.binder.configuration:: -Key/Value map of client properties (both producers and consumer) passed to all clients created by the binder. -Due to the fact that these properties are used by both producers and consumers, usage should be restricted to common properties -- for example, security settings. -Unknown Kafka producer or consumer properties provided through this configuration are filtered out and not allowed to propagate. -Properties here supersede any properties set in boot. -+ -Default: Empty map. -spring.cloud.stream.kafka.binder.consumerProperties:: -Key/Value map of arbitrary Kafka client consumer properties. -In addition to support known Kafka consumer properties, unknown consumer properties are allowed here as well. -Properties here supersede any properties set in boot and in the `configuration` property above. -+ -Default: Empty map. -spring.cloud.stream.kafka.binder.headers:: -The list of custom headers that are transported by the binder. -Only required when communicating with older applications (<= 1.3.x) with a `kafka-clients` version < 0.11.0.0. Newer versions support headers natively. -+ -Default: empty. -spring.cloud.stream.kafka.binder.healthTimeout:: -The time to wait to get partition information, in seconds. -Health reports as down if this timer expires. -+ -Default: 10. -spring.cloud.stream.kafka.binder.requiredAcks:: -The number of required acks on the broker. -See the Kafka documentation for the producer `acks` property. -+ -Default: `1`. -spring.cloud.stream.kafka.binder.minPartitionCount:: -Effective only if `autoCreateTopics` or `autoAddPartitions` is set. -The global minimum number of partitions that the binder configures on topics on which it produces or consumes data. -It can be superseded by the `partitionCount` setting of the producer or by the value of `instanceCount * concurrency` settings of the producer (if either is larger). -+ -Default: `1`. -spring.cloud.stream.kafka.binder.producerProperties:: -Key/Value map of arbitrary Kafka client producer properties. -In addition to support known Kafka producer properties, unknown producer properties are allowed here as well. -Properties here supersede any properties set in boot and in the `configuration` property above. -+ -Default: Empty map. -spring.cloud.stream.kafka.binder.replicationFactor:: -The replication factor of auto-created topics if `autoCreateTopics` is active. -Can be overridden on each binding. -+ -NOTE: If you are using Kafka broker versions prior to 2.4, then this value should be set to at least `1`. -Starting with version 3.0.8, the binder uses `-1` as the default value, which indicates that the broker 'default.replication.factor' property will be used to determine the number of replicas. -Check with your Kafka broker admins to see if there is a policy in place that requires a minimum replication factor, if that's the case then, typically, the `default.replication.factor` will match that value and `-1` should be used, unless you need a replication factor greater than the minimum. -+ -Default: `-1`. -spring.cloud.stream.kafka.binder.autoCreateTopics:: -If set to `true`, the binder creates new topics automatically. -If set to `false`, the binder relies on the topics being already configured. -In the latter case, if the topics do not exist, the binder fails to start. -+ -NOTE: This setting is independent of the `auto.create.topics.enable` setting of the broker and does not influence it. -If the server is set to auto-create topics, they may be created as part of the metadata retrieval request, with default broker settings. -+ -Default: `true`. -spring.cloud.stream.kafka.binder.autoAddPartitions:: -If set to `true`, the binder creates new partitions if required. -If set to `false`, the binder relies on the partition size of the topic being already configured. -If the partition count of the target topic is smaller than the expected value, the binder fails to start. -+ -Default: `false`. -spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix:: -Enables transactions in the binder. See `transaction.id` in the Kafka documentation and https://docs.spring.io/spring-kafka/reference/html/#transactions[Transactions] in the `spring-kafka` documentation. -When transactions are enabled, individual `producer` properties are ignored and all producers use the `spring.cloud.stream.kafka.binder.transaction.producer.*` properties. -+ -Default `null` (no transactions) -spring.cloud.stream.kafka.binder.transaction.producer.*:: -Global producer properties for producers in a transactional binder. -See `spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix` and xref:kafka/kafka_overview/configuration-options.adoc#kafka-producer-properties[Kafka Producer Properties] and the general producer properties supported by all binders. -+ -Default: See individual producer properties. - -spring.cloud.stream.kafka.binder.headerMapperBeanName:: -The bean name of a `KafkaHeaderMapper` used for mapping `spring-messaging` headers to and from Kafka headers. -Use this, for example, if you wish to customize the trusted packages in a `BinderHeaderMapper` bean that uses JSON deserialization for the headers. -If this custom `BinderHeaderMapper` bean is not made available to the binder using this property, then the binder will look for a header mapper bean with the name `kafkaBinderHeaderMapper` that is of type `BinderHeaderMapper` before falling back to a default `BinderHeaderMapper` created by the binder. -+ -Default: none. - -spring.cloud.stream.kafka.binder.considerDownWhenAnyPartitionHasNoLeader:: -Flag to set the binder health as `down`, when any partitions on the topic, regardless of the consumer that is receiving data from it, is found without a leader. -+ -Default: `false`. - -spring.cloud.stream.kafka.binder.certificateStoreDirectory:: -When the truststore or keystore certificate location is given as a non-local file system resource (resources supported by org.springframework.core.io.Resource e.g. CLASSPATH, HTTP, etc.), -the binder copies the resource from the path (which is convertible to org.springframework.core.io.Resource) to a location on the filesystem. -This is true for both broker level certificates (`ssl.truststore.location` and `ssl.keystore.location`) and certificates intended for schema registry (`schema.registry.ssl.truststore.location` and `schema.registry.ssl.keystore.location`). -Keep in mind that the truststore and keystore location paths must be provided under `spring.cloud.stream.kafka.binder.configuration...`. -For example, `spring.cloud.stream.kafka.binder.configuration.ssl.truststore.location`, `spring.cloud.stream.kafka.binder.configuration.schema.registry.ssl.truststore.location`, etc. -The file will be copied to the location specified as the value for this property which must be an existing directory on the filesystem that is writable by the process running the application. -If this value is not set and the certificate file is a non-local file system resource, then it will be copied to System's temp directory as returned by `System.getProperty("java.io.tmpdir")`. -This is also true, if this value is present, but the directory cannot be found on the filesystem or is not writable. -+ -Default: none. - -spring.cloud.stream.kafka.binder.metrics.defaultOffsetLagMetricsEnabled:: -When set to true, the offset lag metric of each consumer topic is computed whenever the metric is accessed. -When set to false only the periodically calculated offset lag is used. -+ -Default: true - -spring.cloud.stream.kafka.binder.metrics.offsetLagMetricsInterval:: -The interval in which the offset lag for each consumer topic is computed. -This value is used whenever `metrics.defaultOffsetLagMetricsEnabled` is disabled or its -computation is taking too long. -+ -Default: 60 seconds - -spring.cloud.stream.kafka.binder.enableObservation:: -Enable Micrometer observation registry on all the bindings in this binder. -+ -Default: false - -[[kafka-consumer-properties]] -== Kafka Consumer Properties - -NOTE: To avoid repetition, Spring Cloud Stream supports setting values for all channels, in the format of `spring.cloud.stream.kafka.default.consumer.=`. - - -The following properties are available for Kafka consumers only and -must be prefixed with `spring.cloud.stream.kafka.bindings..consumer.`. - -admin.configuration:: -Since version 2.1.1, this property is deprecated in favor of `topic.properties`, and support for it will be removed in a future version. - -admin.replicas-assignment:: -Since version 2.1.1, this property is deprecated in favor of `topic.replicas-assignment`, and support for it will be removed in a future version. - -admin.replication-factor:: -Since version 2.1.1, this property is deprecated in favor of `topic.replication-factor`, and support for it will be removed in a future version. - -autoRebalanceEnabled:: -When `true`, topic partitions is automatically rebalanced between the members of a consumer group. -When `false`, each consumer is assigned a fixed set of partitions based on `spring.cloud.stream.instanceCount` and `spring.cloud.stream.instanceIndex`. -This requires both the `spring.cloud.stream.instanceCount` and `spring.cloud.stream.instanceIndex` properties to be set appropriately on each launched instance. -The value of the `spring.cloud.stream.instanceCount` property must typically be greater than 1 in this case. -+ -Default: `true`. -ackEachRecord:: -When `autoCommitOffset` is `true`, this setting dictates whether to commit the offset after each record is processed. -By default, offsets are committed after all records in the batch of records returned by `consumer.poll()` have been processed. -The number of records returned by a poll can be controlled with the `max.poll.records` Kafka property, which is set through the consumer `configuration` property. -Setting this to `true` may cause a degradation in performance, but doing so reduces the likelihood of redelivered records when a failure occurs. -Also, see the binder `requiredAcks` property, which also affects the performance of committing offsets. -This property is deprecated as of 3.1 in favor of using `ackMode`. -If the `ackMode` is not set and batch mode is not enabled, `RECORD` ackMode will be used. -+ -Default: `false`. - -autoCommitOffset:: - -Starting with version 3.1, this property is deprecated. -See `ackMode` for more details on alternatives. -Whether to autocommit offsets when a message has been processed. -If set to `false`, a header with the key `kafka_acknowledgment` of the type `org.springframework.kafka.support.Acknowledgment` header is present in the inbound message. -Applications may use this header for acknowledging messages. -See the examples section for details. -When this property is set to `false`, Kafka binder sets the ack mode to `org.springframework.kafka.listener.AbstractMessageListenerContainer.AckMode.MANUAL` and the application is responsible for acknowledging records. -Also see `ackEachRecord`. -+ -Default: `true`. -ackMode:: -Specify the container ack mode. -This is based on the AckMode enumeration defined in Spring Kafka. -If `ackEachRecord` property is set to `true` and consumer is not in batch mode, then this will use the ack mode of `RECORD`, otherwise, use the provided ack mode using this property. - -autoCommitOnError:: -In pollable consumers, if set to `true`, it always auto commits on error. -If not set (the default) or false, it will not auto commit in pollable consumers. -Note that this property is only applicable for pollable consumers. -+ -Default: not set. -resetOffsets:: -Whether to reset offsets on the consumer to the value provided by startOffset. -Must be false if a `KafkaBindingRebalanceListener` is provided; see xref:kafka/kafka_overview/rebalance-listener.adoc[Using a KafkaBindingRebalanceListener]. -See xref:kafka/kafka_overview/configuration-options.adoc#reset-offsets[Resetting Offsets] for more information about this property. -+ -Default: `false`. -startOffset:: -The starting offset for new groups. -Allowed values: `earliest` and `latest`. -If the consumer group is set explicitly for the consumer 'binding' (through `spring.cloud.stream.bindings..group`), 'startOffset' is set to `earliest`. Otherwise, it is set to `latest` for the `anonymous` consumer group. -See xref:kafka/kafka_overview/configuration-options.adoc#reset-offsets[Resetting Offsets] for more information about this property. -+ -Default: null (equivalent to `earliest`). -enableDlq:: -When set to true, it enables DLQ behavior for the consumer. -By default, messages that result in errors are forwarded to a topic named `error..`. -The DLQ topic name can be configurable by setting the `dlqName` property or by defining a `@Bean` of type `DlqDestinationResolver`. -This provides an alternative option to the more common Kafka replay scenario for the case when the number of errors is relatively small and replaying the entire original topic may be too cumbersome. -See xref:kafka/kafka_dlq.adoc[Dead-Letter Topic Processing] processing for more information. -Starting with version 2.0, messages sent to the DLQ topic are enhanced with the following headers: `x-original-topic`, `x-exception-message`, and `x-exception-stacktrace` as `byte[]`. -By default, a failed record is sent to the same partition number in the DLQ topic as the original record. -See xref:kafka/kafka_dlq.adoc#dlq-partition-selection[Dead-Letter Topic Partition Selection] for how to change that behavior. -**Not allowed when `destinationIsPattern` is `true`.** -+ -Default: `false`. -dlqPartitions:: -When `enableDlq` is true, and this property is not set, a dead letter topic with the same number of partitions as the primary topic(s) is created. -Usually, dead-letter records are sent to the same partition in the dead-letter topic as the original record. -This behavior can be changed; see xref:kafka/kafka_dlq.adoc#dlq-partition-selection[Dead-Letter Topic Partition Selection]. -If this property is set to `1` and there is no `DqlPartitionFunction` bean, all dead-letter records will be written to partition `0`. -If this property is greater than `1`, you **MUST** provide a `DlqPartitionFunction` bean. -Note that the actual partition count is affected by the binder's `minPartitionCount` property. -+ -Default: `none` -configuration:: -Map with a key/value pair containing generic Kafka consumer properties. -In addition to having Kafka consumer properties, other configuration properties can be passed here. -For example some properties needed by the application such as `spring.cloud.stream.kafka.bindings.input.consumer.configuration.foo=bar`. -The `bootstrap.servers` property cannot be set here; use multi-binder support if you need to connect to multiple clusters. -+ -Default: Empty map. -dlqName:: -The name of the DLQ topic to receive the error messages. -+ -Default: null (If not specified, messages that result in errors are forwarded to a topic named `error..`). -dlqProducerProperties:: -Using this, DLQ-specific producer properties can be set. -All the properties available through kafka producer properties can be set through this property. -When native decoding is enabled on the consumer (i.e., useNativeDecoding: true) , the application must provide corresponding key/value serializers for DLQ. -This must be provided in the form of `dlqProducerProperties.configuration.key.serializer` and `dlqProducerProperties.configuration.value.serializer`. -+ -Default: Default Kafka producer properties. -standardHeaders:: -Indicates which standard headers are populated by the inbound channel adapter. -Allowed values: `none`, `id`, `timestamp`, or `both`. -Useful if using native deserialization and the first component to receive a message needs an `id` (such as an aggregator that is configured to use a JDBC message store). -+ -Default: `none` -converterBeanName:: -The name of a bean that implements `RecordMessageConverter`. Used in the inbound channel adapter to replace the default `MessagingMessageConverter`. -+ -Default: `null` -idleEventInterval:: -The interval, in milliseconds, between events indicating that no messages have recently been received. -Use an `ApplicationListener` to receive these events. -See xref:kafka/kafka_overview/configuration-options.adoc#pause-resume[Example: Pausing and Resuming the Consumer] for a usage example. -+ -Default: `30000` -destinationIsPattern:: -When true, the destination is treated as a regular expression `Pattern` used to match topic names by the broker. -When true, topics are not provisioned, and `enableDlq` is not allowed, because the binder does not know the topic names during the provisioning phase. -Note, the time taken to detect new topics that match the pattern is controlled by the consumer property `metadata.max.age.ms`, which (at the time of writing) defaults to 300,000ms (5 minutes). -This can be configured using the `configuration` property above. -+ -Default: `false` -topic.properties:: -A `Map` of Kafka topic properties used when provisioning new topics -- for example, `spring.cloud.stream.kafka.bindings.input.consumer.topic.properties.message.format.version=0.9.0.0` -+ -Default: none. -topic.replicas-assignment:: -A Map> of replica assignments, with the key being the partition and the value being the assignments. -Used when provisioning new topics. -See the `NewTopic` Javadocs in the `kafka-clients` jar. -+ -Default: none. -topic.replication-factor:: -The replication factor to use when provisioning topics. Overrides the binder-wide setting. -Ignored if `replicas-assignments` is present. -+ -Default: none (the binder-wide default of -1 is used). -pollTimeout:: -Timeout used for polling in pollable consumers. -+ -Default: 5 seconds. -transactionManager:: -Bean name of a `KafkaAwareTransactionManager` used to override the binder's transaction manager for this binding. -Usually needed if you want to synchronize another transaction with the Kafka transaction, using the `ChainedKafkaTransactionManaager`. -To achieve exactly once consumption and production of records, the consumer and producer bindings must all be configured with the same transaction manager. -+ -Default: none. -txCommitRecovered:: -When using a transactional binder, the offset of a recovered record (e.g. when retries are exhausted and the record is sent to a dead letter topic) will be committed via a new transaction, by default. -Setting this property to `false` suppresses committing the offset of recovered record. -+ -Default: true. -commonErrorHandlerBeanName:: -`CommonErrorHandler` bean name to use per consumer binding. -When present, this user provided `CommonErrorHandler` takes precedence over any other error handlers defined by the binder. -This is a handy way to express error handlers, if the application does not want to use a `ListenerContainerCustomizer` and then check the destination/group combination to set an error handler. -+ -Default: none. - - -[[reset-offsets]] -== Resetting Offsets - -When an application starts, the initial position in each assigned partition depends on two properties `startOffset` and `resetOffsets`. -If `resetOffsets` is `false`, normal Kafka consumer https://kafka.apache.org/documentation/#consumerconfigs_auto.offset.reset[`auto.offset.reset`] semantics apply. -i.e. If there is no committed offset for a partition for the binding's consumer group, the position is `earliest` or `latest`. -By default, bindings with an explicit `group` use `earliest`, and anonymous bindings (with no `group`) use `latest`. -These defaults can be overridden by setting the `startOffset` binding property. -There will be no committed offset(s) the first time the binding is started with a particular `group`. -The other condition where no committed offset exists is if the offset has been expired. -With modern brokers (since 2.1), and default broker properties, the offsets are expired 7 days after the last member leaves the group. -See the https://kafka.apache.org/documentation/#brokerconfigs_offsets.retention.minutes[`offsets.retention.minutes`] broker property for more information. - -When `resetOffsets` is `true`, the binder applies similar semantics to those that apply when there is no committed offset on the broker, as if this binding has never consumed from the topic; i.e. any current committed offset is ignored. - -Following are two use cases when this might be used. - -1. Consuming from a compacted topic containing key/value pairs. -Set `resetOffsets` to `true` and `startOffset` to `earliest`; the binding will perform a `seekToBeginning` on all newly assigned partitions. - -2. Consuming from a topic containing events, where you are only interested in events that occur while this binding is running. -Set `resetOffsets` to `true` and `startOffset` to `latest`; the binding will perform a `seekToEnd` on all newly assigned partitions. - -IMPORTANT: If a rebalance occurs after the initial assignment, the seeks will only be performed on any newly assigned partitions that were not assigned during the initial assignment. - -For more control over topic offsets, see xref:kafka/kafka_overview/rebalance-listener.adoc[Using a KafkaBindingRebalanceListener]; when a listener is provided, `resetOffsets` should not be set to `true`, otherwise, that will cause an error. - -[[consuming-batches]] -== Consuming Batches - -Starting with version 3.0, when `spring.cloud.stream.bindings..consumer.batch-mode` is set to `true`, all of the records received by polling the Kafka `Consumer` will be presented as a `List` to the listener method. -Otherwise, the method will be called with one record at a time. -The size of the batch is controlled by Kafka consumer properties `max.poll.records`, `fetch.min.bytes`, `fetch.max.wait.ms`; refer to the Kafka documentation for more information. - -Starting with version `4.0.2`, the binder supports DLQ capabilities when consuming in batch mode. -Keep in mind that, when using DLQ on a consumer binding that is in batch mode, all the records received from the previous poll will be delivered to the DLQ topic. - -IMPORTANT: Retry within the binder is not supported when using batch mode, so `maxAttempts` will be overridden to 1. -You can configure a `DefaultErrorHandler` (using a `ListenerContainerCustomizer`) to achieve similar functionality to retry in the binder. -You can also use a manual `AckMode` and call `Ackowledgment.nack(index, sleep)` to commit the offsets for a partial batch and have the remaining records redelivered. -Refer to the https://docs.spring.io/spring-kafka/docs/2.3.15.BUILD-SNAPSHOT/reference/html/#committing-offsets[Spring for Apache Kafka documentation] for more information about these techniques. - -[[kafka-producer-properties]] -== Kafka Producer Properties - -NOTE: To avoid repetition, Spring Cloud Stream supports setting values for all channels, in the format of `spring.cloud.stream.kafka.default.producer.=`. - - -The following properties are available for Kafka producers only and -must be prefixed with `spring.cloud.stream.kafka.bindings..producer.`. - -admin.configuration:: -Since version 2.1.1, this property is deprecated in favor of `topic.properties`, and support for it will be removed in a future version. - -admin.replicas-assignment:: -Since version 2.1.1, this property is deprecated in favor of `topic.replicas-assignment`, and support for it will be removed in a future version. - -admin.replication-factor:: -Since version 2.1.1, this property is deprecated in favor of `topic.replication-factor`, and support for it will be removed in a future version. - -bufferSize:: -Upper limit, in bytes, of how much data the Kafka producer attempts to batch before sending. -+ -Default: `16384`. -sync:: -Whether the producer is synchronous. -+ -Default: `false`. -sendTimeoutExpression:: -A SpEL expression evaluated against the outgoing message used to evaluate the time to wait for ack when synchronous publish is enabled -- for example, `headers['mySendTimeout']`. -The value of the timeout is in milliseconds. -With versions before 3.0, the payload could not be used unless native encoding was being used because, by the time this expression was evaluated, the payload was already in the form of a `byte[]`. -Now, the expression is evaluated before the payload is converted. -+ -Default: `none`. -batchTimeout:: -How long the producer waits to allow more messages to accumulate in the same batch before sending the messages. -(Normally, the producer does not wait at all and simply sends all the messages that accumulated while the previous send was in progress.) A non-zero value may increase throughput at the expense of latency. -+ -Default: `0`. -messageKeyExpression:: -A SpEL expression evaluated against the outgoing message used to populate the key of the produced Kafka message -- for example, `headers['myKey']`. -With versions before 3.0, the payload could not be used unless native encoding was being used because, by the time this expression was evaluated, the payload was already in the form of a `byte[]`. -Now, the expression is evaluated before the payload is converted. -In the case of a regular processor (`Function` or `Function, Message`), if the produced key needs to be same as the incoming key from the topic, this property can be set as below. -`spring.cloud.stream.kafka.bindings..producer.messageKeyExpression: headers['kafka_receivedMessageKey']` -There is an important caveat to keep in mind for reactive functions. -In that case, it is up to the application to manually copy the headers from the incoming messages to outbound messages. -You can set the header, e.g. `myKey` and use `headers['myKey']` as suggested above or, for convenience, simply set the `KafkaHeaders.MESSAGE_KEY` header, and you do not need to set this property at all. -+ -Default: `none`. -headerPatterns:: -A comma-delimited list of simple patterns to match Spring messaging headers to be mapped to the Kafka `Headers` in the `ProducerRecord`. -Patterns can begin or end with the wildcard character (asterisk). -Patterns can be negated by prefixing with `!`. -Matching stops after the first match (positive or negative). -For example `!ask,as*` will pass `ash` but not `ask`. -`id` and `timestamp` are never mapped. -+ -Default: `*` (all headers - except the `id` and `timestamp`) -configuration:: -Map with a key/value pair containing generic Kafka producer properties. -The `bootstrap.servers` property cannot be set here; use multi-binder support if you need to connect to multiple clusters. -+ -Default: Empty map. -topic.properties:: -A `Map` of Kafka topic properties used when provisioning new topics -- for example, `spring.cloud.stream.kafka.bindings.output.producer.topic.properties.message.format.version=0.9.0.0` -+ -topic.replicas-assignment:: -A Map> of replica assignments, with the key being the partition and the value being the assignments. -Used when provisioning new topics. -See the `NewTopic` Javadocs in the `kafka-clients` jar. -+ -Default: none. -topic.replication-factor:: -The replication factor to use when provisioning topics. Overrides the binder-wide setting. -Ignored if `replicas-assignments` is present. -+ -Default: none (the binder-wide default of -1 is used). -useTopicHeader:: -Set to `true` to override the default binding destination (topic name) with the value of the `KafkaHeaders.TOPIC` message header in the outbound message. -If the header is not present, the default binding destination is used. -+ -Default: `false`. -recordMetadataChannel:: -The bean name of a `MessageChannel` to which successful send results should be sent; the bean must exist in the application context. -The message sent to the channel is the sent message (after conversion, if any) with an additional header `KafkaHeaders.RECORD_METADATA`. -The header contains a `RecordMetadata` object provided by the Kafka client; it includes the partition and offset where the record was written in the topic. -+ -`ResultMetadata meta = sendResultMsg.getHeaders().get(KafkaHeaders.RECORD_METADATA, RecordMetadata.class)` -+ -Failed sends go the producer error channel (if configured); see xref:kafka/kafka_overview/error-channels.adoc[Error Channels]. -+ -Default: null. - -NOTE: The Kafka binder uses the `partitionCount` setting of the producer as a hint to create a topic with the given partition count (in conjunction with the `minPartitionCount`, the maximum of the two being the value being used). -Exercise caution when configuring both `minPartitionCount` for a binder and `partitionCount` for an application, as the larger value is used. -If a topic already exists with a smaller partition count and `autoAddPartitions` is disabled (the default), the binder fails to start. -If a topic already exists with a smaller partition count and `autoAddPartitions` is enabled, new partitions are added. -If a topic already exists with a larger number of partitions than the maximum of (`minPartitionCount` or `partitionCount`), the existing partition count is used. - -compression:: -Set the `compression.type` producer property. -Supported values are `none`, `gzip`, `snappy`, `lz4` and `zstd`. -If you override the `kafka-clients` jar to 2.1.0 (or later), as discussed in the https://docs.spring.io/spring-kafka/docs/2.2.x/reference/html/#deps-for-21x[Spring for Apache Kafka documentation], and wish to use `zstd` compression, use `spring.cloud.stream.kafka.bindings..producer.configuration.compression.type=zstd`. -+ -Default: `none`. -transactionManager:: -Bean name of a `KafkaAwareTransactionManager` used to override the binder's transaction manager for this binding. -Usually needed if you want to synchronize another transaction with the Kafka transaction, using the `ChainedKafkaTransactionManaager`. -To achieve exactly once consumption and production of records, the consumer and producer bindings must all be configured with the same transaction manager. -+ -Default: none. - -closeTimeout:: -Timeout in number of seconds to wait for when closing the producer. -+ -Default: `30` - -allowNonTransactional:: -Normally, all output bindings associated with a transactional binder will publish in a new transaction, if one is not already in process. -This property allows you to override that behavior. -If set to true, records published to this output binding will not be run in a transaction, unless one is already in process. -+ -Default: `false` - -[[usage-examples]] -== Usage examples - -In this section, we show the use of the preceding properties for specific scenarios. - -[[example:-setting-ackmode-to-manual-and-relying-on-manual-acknowledgement]] -=== Example: Setting `ackMode` to `MANUAL` and Relying on Manual Acknowledgement - -This example illustrates how one may manually acknowledge offsets in a consumer application. - -This example requires that `spring.cloud.stream.kafka.bindings.input.consumer.ackMode` be set to `MANUAL`. -Use the corresponding input channel name for your example. - -[source] ----- -@SpringBootApplication -public class ManuallyAcknowdledgingConsumer { - - public static void main(String[] args) { - SpringApplication.run(ManuallyAcknowdledgingConsumer.class, args); - } - - @Bean - public Consumer> process() { - return message -> { - Acknowledgment acknowledgment = message.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class); - if (acknowledgment != null) { - System.out.println("Acknowledgment provided"); - acknowledgment.acknowledge(); - } - }; -} ----- - -[[example:-security-configuration]] -=== Example: Security Configuration - -Apache Kafka 0.9 supports secure connections between client and brokers. -To take advantage of this feature, follow the guidelines in the https://kafka.apache.org/090/documentation.html#security_configclients[Apache Kafka Documentation] as well as the Kafka 0.9 https://docs.confluent.io/2.0.0/kafka/security.html[security guidelines from the Confluent documentation]. -Use the `spring.cloud.stream.kafka.binder.configuration` option to set security properties for all clients created by the binder. - -For example, to set `security.protocol` to `SASL_SSL`, set the following property: - -[source] ----- -spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_SSL ----- - -All the other security properties can be set in a similar manner. - -When using Kerberos, follow the instructions in the https://kafka.apache.org/090/documentation.html#security_sasl_clientconfig[reference documentation] for creating and referencing the JAAS configuration. - -Spring Cloud Stream supports passing JAAS configuration information to the application by using a JAAS configuration file and using Spring Boot properties. - -[[using-jaas-configuration-files]] -==== Using JAAS Configuration Files - -The JAAS and (optionally) krb5 file locations can be set for Spring Cloud Stream applications by using system properties. -The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using a JAAS configuration file: - -[source,bash] ----- - java -Djava.security.auth.login.config=/path.to/kafka_client_jaas.conf -jar log.jar \ - --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \ - --spring.cloud.stream.bindings.input.destination=stream.ticktock \ - --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT ----- - -[[using-spring-boot-properties]] -==== Using Spring Boot Properties - -As an alternative to having a JAAS configuration file, Spring Cloud Stream provides a mechanism for setting up the JAAS configuration for Spring Cloud Stream applications by using Spring Boot properties. - -The following properties can be used to configure the login context of the Kafka client: - -spring.cloud.stream.kafka.binder.jaas.loginModule:: -The login module name. Not necessary to be set in normal cases. -+ -Default: `com.sun.security.auth.module.Krb5LoginModule`. -spring.cloud.stream.kafka.binder.jaas.controlFlag:: -The control flag of the login module. -+ -Default: `required`. -spring.cloud.stream.kafka.binder.jaas.options:: -Map with a key/value pair containing the login module options. -+ -Default: Empty map. - -The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using Spring Boot configuration properties: - -[source,bash] ----- - java --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \ - --spring.cloud.stream.bindings.input.destination=stream.ticktock \ - --spring.cloud.stream.kafka.binder.autoCreateTopics=false \ - --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT \ - --spring.cloud.stream.kafka.binder.jaas.options.useKeyTab=true \ - --spring.cloud.stream.kafka.binder.jaas.options.storeKey=true \ - --spring.cloud.stream.kafka.binder.jaas.options.keyTab=/etc/security/keytabs/kafka_client.keytab \ - --spring.cloud.stream.kafka.binder.jaas.options.principal=kafka-client-1@EXAMPLE.COM ----- - -The preceding example represents the equivalent of the following JAAS file: - -[source] ----- -KafkaClient { - com.sun.security.auth.module.Krb5LoginModule required - useKeyTab=true - storeKey=true - keyTab="/etc/security/keytabs/kafka_client.keytab" - principal="kafka-client-1@EXAMPLE.COM"; -}; ----- - -If the topics required already exist on the broker or will be created by an administrator, autocreation can be turned off and only client JAAS properties need to be sent. - -NOTE: Do not mix JAAS configuration files and Spring Boot properties in the same application. -If the `-Djava.security.auth.login.config` system property is already present, Spring Cloud Stream ignores the Spring Boot properties. - -NOTE: Be careful when using the `autoCreateTopics` and `autoAddPartitions` with Kerberos. -Usually, applications may use principals that do not have administrative rights in Kafka and Zookeeper. -Consequently, relying on Spring Cloud Stream to create/modify topics may fail. -In secure environments, we strongly recommend creating topics and managing ACLs administratively by using Kafka tooling. - -[[multi-binder-configuration-and-jaas]] -==== Multi-binder configuration and JAAS - -When connecting to multiple clusters in which each one requires separate JAAS configuration, then set the JAAS configuration using the property `sasl.jaas.config`. -When this property is present in the application, it takes precedence over the other strategies mentioned above. -See this https://cwiki.apache.org/confluence/display/KAFKA/KIP-85%3A+Dynamic+JAAS+configuration+for+Kafka+clients[KIP-85] for more details. - -For example, if you have two clusters in your application with separate JAAS configuration, then the following is a template that you can use: - -``` -spring.cloud.stream: - binders: - kafka1: - type: kafka - environment: - spring: - cloud: - stream: - kafka: - binder: - brokers: localhost:9092 - configuration.sasl.jaas.config: "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin-secret\";" - kafka2: - type: kafka - environment: - spring: - cloud: - stream: - kafka: - binder: - brokers: localhost:9093 - configuration.sasl.jaas.config: "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"user1\" password=\"user1-secret\";" - kafka.binder: - configuration: - security.protocol: SASL_PLAINTEXT - sasl.mechanism: PLAIN -``` - -Note that both the Kafka clusters, and the `sasl.jaas.config` values for each of them are different in the above configuration. - -See this https://github.com/spring-cloud/spring-cloud-stream-samples/tree/main/multi-binder-samples/kafka-multi-binder-jaas[sample application] for more details on how to setup and run such an application. - -[[pause-resume]] -=== Example: Pausing and Resuming the Consumer - -If you wish to suspend consumption but not cause a partition rebalance, you can pause and resume the consumer. -This is facilitated by managing the binding lifecycle as shown in **Binding visualization and control** in the Spring Cloud Stream documentation, using `State.PAUSED` and `State.RESUMED`. - -To resume, you can use an `ApplicationListener` (or `@EventListener` method) to receive `ListenerContainerIdleEvent` instances. -The frequency at which events are published is controlled by the `idleEventInterval` property. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/consumer-producer-config-customizer.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/consumer-producer-config-customizer.adoc deleted file mode 100644 index 2e48486cc..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/consumer-producer-config-customizer.adoc +++ /dev/null @@ -1,16 +0,0 @@ -[[consumer-producer-config-customizer]] -= Customizing Consumer and Producer configuration -:page-section-summary-toc: 1 - -If you want advanced customization of consumer and producer configuration that is used for creating `ConsumerFactory` and `ProducerFactory` in Kafka, -you can implement the following customizers. - -* ConsumerConfigCustomizer -* ProducerConfigCustomizer - -Both of these interfaces provide a way to configure the config map used for consumer and producer properties. -For example, if you want to gain access to a bean that is defined at the application level, you can inject that in the implementation of the `configure` method. -When the binder discovers that these customizers are available as beans, it will invoke the `configure` method right before creating the consumer and producer factories. - -Both of these interfaces also provide access to both the binding and destination names so that they can be accessed while customizing producer and consumer properties. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/custom-binder-health-indicator-example.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/custom-binder-health-indicator-example.adoc deleted file mode 100644 index e3067edfa..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/custom-binder-health-indicator-example.adoc +++ /dev/null @@ -1,55 +0,0 @@ -[[custom-kafka-binder-health-indicator-example]] -= Custom kafka Binder Health Indicator Example - -Here is the pseudo-code for writing a custom Kafka binder HealthIndicator. -In this example, we try to override the binder provided Kafka HealthIndicator by specifically checking first for cluster connectivity and then followed by topic-related issues. - - -1. First we need create a custom implementation of the `KafkaBinderHealth` interface. - -``` -public class KafkaBinderHealthImplementation implements KafkaBinderHealth { - @Value("${spring.cloud.bus.destination}") - private String topic; - private final AdminClient client; - - public KafkaBinderHealthImplementation(final KafkaAdmin admin) { - // More about configuring Kafka - // https://docs.spring.io/spring-kafka/reference/html/#configuring-topics - this.client = AdminClient.create(admin.getConfigurationProperties()); - } - - @Override - public Health health() { - if (!checkBrokersConnection()) { - logger.error("Error when connect brokers"); - return Health.down().withDetail("BrokersConnectionError", "Error message").build(); - } - if (!checkTopicConnection()) { - logger.error("Error when trying to connect with specific topic"); - return Health.down().withDetail("TopicError", "Error message with topic name").build(); - } - return Health.up().build(); - } - - public boolean checkBrokersConnection() { - // Your implementation - } - - public boolean checkTopicConnection() { - // Your implementation - } -} -``` - -2. Then we need to create a bean for the custom implementation. - -``` -@Configuration -public class KafkaBinderHealthIndicatorConfiguration { - @Bean - public KafkaBinderHealth kafkaBinderHealthIndicator(final KafkaAdmin admin) { - return new KafkaBinderHealthImplementation(admin); - } -} -``` diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/custom-binder-health-indicator.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/custom-binder-health-indicator.adoc deleted file mode 100644 index f8f8506d5..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/custom-binder-health-indicator.adoc +++ /dev/null @@ -1,25 +0,0 @@ -[[custom-kafka-binder-health-indicator]] -= Custom Kafka Binder Health Indicator -:page-section-summary-toc: 1 - -Kafka binder activates a default health indicator when Spring Boot actuator is on the classpath. -This health indicator checks the health of the binder and any communication issues with the Kafka broker. -If an application wants to disable this default health check implementation and include a custom implementation, then it can provide an implementation for `KafkaBinderHealth` interface. -`KafkaBinderHealth` is a marker interface that extends from `HealthIndicator`. -In the custom implementation, it must provide an implementation for the `health()` method. -The custom implementation must be present in the application configuration as a bean. -When the binder discovers the custom implementation, it will use that instead of the default implementation. -Here is an example of such a custom implementation bean in the application. - -``` -@Bean -public KafkaBinderHealth kafkaBinderHealthIndicator() { - return new KafkaBinderHealth() { - @Override - public Health health() { - // custom implementation details. - } - }; -} -``` - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/error-channels.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/error-channels.adoc deleted file mode 100644 index cfc6faa9d..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/error-channels.adoc +++ /dev/null @@ -1,15 +0,0 @@ -[[kafka-error-channels]] -= Error Channels -:page-section-summary-toc: 1 - -Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. -See https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/current/reference/html/spring-cloud-stream.html#spring-cloud-stream-overview-error-handling[this section on error handling] for more information. - -The payload of the `ErrorMessage` for a send failure is a `KafkaSendFailureException` with properties: - -* `failedMessage`: The Spring Messaging `Message` that failed to be sent. -* `record`: The raw `ProducerRecord` that was created from the `failedMessage` - -There is no automatic handling of producer exceptions (such as sending to a xref:kafka/kafka_dlq.adoc[Dead-Letter queue]). -You can consume these exceptions with your own Spring Integration flow. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/metrics.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/metrics.adoc deleted file mode 100644 index 56d0e4528..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/metrics.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[kafka-metrics]] -= Kafka Metrics - -Kafka binder module exposes the following metrics: - -`spring.cloud.stream.binder.kafka.offset`: This metric indicates how many messages have not been yet consumed from a given binder's topic by a given consumer group. -The metrics provided are based on the Micrometer library. -The binder creates the `KafkaBinderMetrics` bean if Micrometer is on the classpath and no other such beans provided by the application. -The metric contains the consumer group information, topic and the actual lag in committed offset from the latest offset on the topic. -This metric is particularly useful for providing auto-scaling feedback to a PaaS platform. - -The metric collection behaviour can be configured by setting properties in the `spring.cloud.stream.kafka.binder.metrics` namespace, -refer to the xref:kafka/kafka_overview/configuration-options.adoc#kafka-binder-properties[kafka binder properties section] for more information. - -You can exclude `KafkaBinderMetrics` from creating the necessary infrastructure like consumers and then reporting the metrics by providing the following component in the application. - -``` -@Component -class NoOpBindingMeters { - NoOpBindingMeters(MeterRegistry registry) { - registry.config().meterFilter( - MeterFilter.denyNameStartsWith(KafkaBinderMetrics.OFFSET_LAG_METRIC_NAME)); - } -} -``` - -More details on how to suppress meters selectively can be found https://micrometer.io/docs/concepts#_meter_filters[here]. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/overview.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/overview.adoc deleted file mode 100644 index 387bf5ad1..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/overview.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[overview]] -= Overview -:page-section-summary-toc: 1 - -The following image shows a simplified diagram of how the Apache Kafka binder operates: - -.Kafka Binder -image::{github-raw}/docs/src/main/asciidoc/images/kafka-binder.png[width=300,scaledwidth="50%"] - -The Apache Kafka Binder implementation maps each destination to an Apache Kafka topic. -The consumer group maps directly to the same Apache Kafka concept. -Partitioning also maps directly to Apache Kafka partitions as well. - -The binder currently uses the Apache Kafka `kafka-clients` version `3.1.0`. -This client can communicate with older brokers (see the Kafka documentation), but certain features may not be available. -For example, with versions earlier than 0.11.x.x, native headers are not supported. -Also, 0.11.x.x does not support the `autoAddPartitions` property. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/rebalance-listener.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/rebalance-listener.adoc deleted file mode 100644 index ffc80c169..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/rebalance-listener.adoc +++ /dev/null @@ -1,51 +0,0 @@ -[[rebalance-listener]] -= Using a KafkaBindingRebalanceListener - -Applications may wish to seek topics/partitions to arbitrary offsets when the partitions are initially assigned, or perform other operations on the consumer. -Starting with version 2.1, if you provide a single `KafkaBindingRebalanceListener` bean in the application context, it will be wired into all Kafka consumer bindings. - -==== -[source, java] ----- -public interface KafkaBindingRebalanceListener { - - /** - * Invoked by the container before any pending offsets are committed. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - */ - default void onPartitionsRevokedBeforeCommit(String bindingName, Consumer consumer, - Collection partitions) { - - } - - /** - * Invoked by the container after any pending offsets are committed. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - */ - default void onPartitionsRevokedAfterCommit(String bindingName, Consumer consumer, Collection partitions) { - - } - - /** - * Invoked when partitions are initially assigned or after a rebalance. - * Applications might only want to perform seek operations on an initial assignment. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - * @param initial true if this is the initial assignment. - */ - default void onPartitionsAssigned(String bindingName, Consumer consumer, Collection partitions, - boolean initial) { - - } - -} ----- -==== - -You cannot set the `resetOffsets` consumer property to `true` when you provide a rebalance listener. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/retry-and-dlq-processing.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/retry-and-dlq-processing.adoc deleted file mode 100644 index d493abb79..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/retry-and-dlq-processing.adoc +++ /dev/null @@ -1,103 +0,0 @@ -[[retry-and-dlq-processing]] -= Retry and Dead Letter Processing - -By default, when you configure retry (e.g. `maxAttemts`) and `enableDlq` in a consumer binding, these functions are performed within the binder, with no participation by the listener container or Kafka consumer. - -There are situations where it is preferable to move this functionality to the listener container, such as: - -* The aggregate of retries and delays will exceed the consumer's `max.poll.interval.ms` property, potentially causing a partition rebalance. -* You wish to publish the dead letter to a different Kafka cluster. -* You wish to add retry listeners to the error handler. -* ... - -To configure moving this functionality from the binder to the container, define a `@Bean` of type `ListenerContainerWithDlqAndRetryCustomizer`. -This interface has the following methods: - -==== -[source, java] ----- -/** - * Configure the container. - * @param container the container. - * @param destinationName the destination name. - * @param group the group. - * @param dlqDestinationResolver a destination resolver for the dead letter topic (if - * enableDlq). - * @param backOff the backOff using retry properties (if configured). - * @see #retryAndDlqInBinding(String, String) - */ -void configure(AbstractMessageListenerContainer container, String destinationName, String group, - @Nullable BiFunction, Exception, TopicPartition> dlqDestinationResolver, - @Nullable BackOff backOff); - -/** - * Return false to move retries and DLQ from the binding to a customized error handler - * using the retry metadata and/or a {@code DeadLetterPublishingRecoverer} when - * configured via - * {@link #configure(AbstractMessageListenerContainer, String, String, BiFunction, BackOff)}. - * @param destinationName the destination name. - * @param group the group. - * @return false to disable retries and DLQ in the binding - */ -default boolean retryAndDlqInBinding(String destinationName, String group) { - return true; -} ----- -==== - -The destination resolver and `BackOff` are created from the binding properties (if configured). The `KafkaTemplate` uses configuration from `spring.kafka....` properties. You can then use these to create a custom error handler and dead letter publisher; for example: - -==== -[source, java] ----- -@Bean -ListenerContainerWithDlqAndRetryCustomizer cust(KafkaTemplate template) { - return new ListenerContainerWithDlqAndRetryCustomizer() { - - @Override - public void configure(AbstractMessageListenerContainer container, String destinationName, - String group, - @Nullable BiFunction, Exception, TopicPartition> dlqDestinationResolver, - @Nullable BackOff backOff) { - - if (destinationName.equals("topicWithLongTotalRetryConfig")) { - ConsumerRecordRecoverer dlpr = new DeadLetterPublishingRecoverer(template, - dlqDestinationResolver); - container.setCommonErrorHandler(new DefaultErrorHandler(dlpr, backOff)); - } - } - - @Override - public boolean retryAndDlqInBinding(String destinationName, String group) { - return !destinationName.contains("topicWithLongTotalRetryConfig"); - } - - }; -} ----- -==== - -Now, only a single retry delay needs to be greater than the consumer's `max.poll.interval.ms` property. - -When working with several binders, the 'ListenerContainerWithDlqAndRetryCustomizer' bean gets overridden by the 'DefaultBinderFactory'. For the bean -to apply, you need to use a 'BinderCustomizer' to set the container customizer (See xref:spring-cloud-stream/binder-customizer.adoc[Customizing binders in multi binder applications]): -==== -[source, java] ----- -@Bean -public BinderCustomizer binderCustomizer(ListenerContainerWithDlqAndRetryCustomizer containerCustomizer) { - return (binder, binderName) -> { - if (binder instanceof KafkaMessageChannelBinder kafkaMessageChannelBinder) { - kafkaMessageChannelBinder.setContainerCustomizer(containerCustomizer); - } - else if (binder instanceof KStreamBinder) { - ... - } - else if (binder instanceof RabbitMessageChannelBinder) { - ... - } - }; -} ----- -==== - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/tombstones.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/tombstones.adoc deleted file mode 100644 index 63b7cb7e3..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/tombstones.adoc +++ /dev/null @@ -1,26 +0,0 @@ -[[kafka-tombstones]] -= Tombstone Records (null record values) -:page-section-summary-toc: 1 - -When using compacted topics, a record with a `null` value (also called a tombstone record) represents the deletion of a key. -To receive such messages in a Spring Cloud Stream function, you can use the following strategy. - -==== -[source, java] ----- -@Bean -public Function, String> myFunction() { - return value -> { - Object v = value.getPayload(); - String className = v.getClass().getName(); - if (className.isEqualTo("org.springframework.kafka.support.KafkaNull")) { - // this is a tombstone record - } - else { - // continue with processing - } - }; -} ----- -==== - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/transactional-binder.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/transactional-binder.adoc deleted file mode 100644 index dbc760267..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/transactional-binder.adoc +++ /dev/null @@ -1,54 +0,0 @@ -[[kafka-transactional-binder]] -= Transactional Binder - -Enable transactions by setting `spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix` to a non-empty value, e.g. `tx-`. -When used in a processor application, the consumer starts the transaction; any records sent on the consumer thread participate in the same transaction. -When the listener exits normally, the listener container will send the offset to the transaction and commit it. -A common producer factory is used for all producer bindings configured using `spring.cloud.stream.kafka.binder.transaction.producer.*` properties; individual binding Kafka producer properties are ignored. - -IMPORTANT: Normal binder retries (and dead lettering) are not supported with transactions because the retries will run in the original transaction, which may be rolled back and any published records will be rolled back too. -When retries are enabled (the common property `maxAttempts` is greater than zero) the retry properties are used to configure a `DefaultAfterRollbackProcessor` to enable retries at the container level. -Similarly, instead of publishing dead-letter records within the transaction, this functionality is moved to the listener container, again via the `DefaultAfterRollbackProcessor` which runs after the main transaction has rolled back. - -If you wish to use transactions in a source application, or from some arbitrary thread for producer-only transaction (e.g. `@Scheduled` method), you must get a reference to the transactional producer factory and define a `KafkaTransactionManager` bean using it. - -==== -[source, java] ----- -@Bean -public PlatformTransactionManager transactionManager(BinderFactory binders, - @Value("${unique.tx.id.per.instance}") String txId) { - - ProducerFactory pf = ((KafkaMessageChannelBinder) binders.getBinder(null, - MessageChannel.class)).getTransactionalProducerFactory(); - KafkaTransactionManager tm = new KafkaTransactionManager<>(pf); - tm.setTransactionId(txId) - return tm; -} ----- -==== - -Notice that we get a reference to the binder using the `BinderFactory`; use `null` in the first argument when there is only one binder configured. -If more than one binder is configured, use the binder name to get the reference. -Once we have a reference to the binder, we can obtain a reference to the `ProducerFactory` and create a transaction manager. - -Then you would use normal Spring transaction support, e.g. `TransactionTemplate` or `@Transactional`, for example: - -==== -[source, java] ----- -public static class Sender { - - @Transactional - public void doInTransaction(MessageChannel output, List stuffToSend) { - stuffToSend.forEach(stuff -> output.send(new GenericMessage<>(stuff))); - } - -} ----- -==== - -If you wish to synchronize producer-only transactions with those from some other transaction manager, use a `ChainedTransactionManager`. - -IMPORTANT: If you deploy multiple instances of your application, each instance needs a unique `transactionIdPrefix`. - diff --git a/docs/modules/ROOT/pages/kafka/kafka_overview/usage.adoc b/docs/modules/ROOT/pages/kafka/kafka_overview/usage.adoc deleted file mode 100644 index 756799ac7..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_overview/usage.adoc +++ /dev/null @@ -1,24 +0,0 @@ -[[usage]] -= Usage -:page-section-summary-toc: 1 - -To use Apache Kafka binder, you need to add `spring-cloud-stream-binder-kafka` as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-kafka - ----- - -Alternatively, you can also use the Spring Cloud Stream Kafka Starter, as shown in the following example for Maven: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-starter-stream-kafka - ----- - diff --git a/docs/modules/ROOT/pages/kafka/kafka_partitions.adoc b/docs/modules/ROOT/pages/kafka/kafka_partitions.adoc deleted file mode 100644 index 382fd5704..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_partitions.adoc +++ /dev/null @@ -1,122 +0,0 @@ -[[partitioning-with-the-kafka-binder]] -= Partitioning with the Kafka Binder - -Apache Kafka supports topic partitioning natively. - -Sometimes it is advantageous to send data to specific partitions -- for example, when you want to strictly order message processing (all messages for a particular customer should go to the same partition). - -The following example shows how to configure the producer and consumer side: - -[source,java] ----- -@SpringBootApplication -public class KafkaPartitionProducerApplication { - - private static final Random RANDOM = new Random(System.currentTimeMillis()); - - private static final String[] data = new String[] { - "foo1", "bar1", "qux1", - "foo2", "bar2", "qux2", - "foo3", "bar3", "qux3", - "foo4", "bar4", "qux4", - }; - - public static void main(String[] args) { - new SpringApplicationBuilder(KafkaPartitionProducerApplication.class) - .web(false) - .run(args); - } - - @Bean - public Supplier> generate() { - return () -> { - String value = data[RANDOM.nextInt(data.length)]; - System.out.println("Sending: " + value); - return MessageBuilder.withPayload(value) - .setHeader("partitionKey", value) - .build(); - }; - } - -} ----- - -.application.yml -[source, yaml] ----- -spring: - cloud: - stream: - bindings: - generate-out-0: - destination: partitioned.topic - producer: - partition-key-expression: headers['partitionKey'] - partition-count: 12 ----- - -IMPORTANT: It is important to keep in mind that, since Apache Kafka supports partitioning natively, there is no need to rely on binder partitioning as described above unless you are using custom partition keys as in the example or an expression that involves the payload itself. -The binder-provided partitioning selection is otherwise intended for middleware technologies that do not support native partitioning. -Note that we are using a custom key called `partitionKey` in the above example, that will be the determining factor for the partition, thus in this case it is appropriate to use binder partitioning. -When using native Kafka partitioning, i.e, when you do not provide the `partition-key-expression`, then Apache Kafka will select a partition, which by default will be the hash value of the record key over the available number of partitions. -To add a key to an outbound record, set the `KafkaHeaders.KEY` header to the desired key value in a spring-messaging `Message`. -By default, when no record key is provided, Apache Kafka will choose a partition based on the logic described in the https://kafka.apache.org/documentation/#producerconfigs_partitioner.class[Apache Kafka Documentation]. - -NOTE: The topic must be provisioned to have enough partitions to achieve the desired concurrency for all consumer groups. -The above configuration supports up to 12 consumer instances (6 if their `concurrency` is 2, 4 if their concurrency is 3, and so on). -It is generally best to "`over-provision`" the partitions to allow for future increases in consumers or concurrency. - -NOTE: The preceding configuration uses the default partitioning (`key.hashCode() % partitionCount`). -This may or may not provide a suitably balanced algorithm, depending on the key values. In particular, note that this partitioning strategy differs from the default used by a standalone Kafka producer - such as the one used by Kafka Streams, meaning that the same key value may balance differently across partitions when produced by those clients. -You can override this default by using the `partitionSelectorExpression` or `partitionSelectorClass` properties. - -Since partitions are natively handled by Kafka, no special configuration is needed on the consumer side. -Kafka allocates partitions across the instances. - -NOTE: The partitionCount for a kafka topic may change during runtime (e.g. due to an adminstration task). -The calculated partitions will be different after that (e.g. new partitions will be used then). -Since 4.0.3 of Spring Cloud Stream runtime changes of partition count will be supported. -See also parameter 'spring.kafka.producer.properties.metadata.max.age.ms' to configure update interval. -Due to some limitations it is not possible to use a 'partition-key-expression' which references the 'payload' of a message, the mechanism will be disabled in that case. -The overall behavior is disabled by default and can be enabled using configuration parameter 'producer.dynamicPartitionUpdatesEnabled=true'. - - -The following Spring Boot application listens to a Kafka stream and prints (to the console) the partition ID to which each message goes: - -[source,java] ----- -@SpringBootApplication -public class KafkaPartitionConsumerApplication { - - public static void main(String[] args) { - new SpringApplicationBuilder(KafkaPartitionConsumerApplication.class) - .web(WebApplicationType.NONE) - .run(args); - } - - @Bean - public Consumer> listen() { - return message -> { - int partition = (int) message.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION); - System.out.println(message + " received from partition " + partition); - }; - } - -} ----- - -.application.yml -[source, yaml] ----- -spring: - cloud: - stream: - bindings: - listen-in-0: - destination: partitioned.topic - group: myGroup ----- - -You can add instances as needed. -Kafka rebalances the partition allocations. -If the instance count (or `instance count * concurrency`) exceeds the number of partitions, some consumers are idle. diff --git a/docs/modules/ROOT/pages/kafka/kafka_tips.adoc b/docs/modules/ROOT/pages/kafka/kafka_tips.adoc deleted file mode 100644 index cbcaeed63..000000000 --- a/docs/modules/ROOT/pages/kafka/kafka_tips.adoc +++ /dev/null @@ -1,911 +0,0 @@ -[[tips-tricks-and-recipes]] -= Tips, Tricks and Recipes - -[[simple-dlq-with-kafka]] -== Simple DLQ with Kafka - -[[problem-statement]] -=== Problem Statement - -As a developer, I want to write a consumer application that processes records from a Kafka topic. -However, if some error occurs in processing, I don't want the application to stop completely. -Instead, I want to send the record in error to a DLT (Dead-Letter-Topic) and then continue processing new records. - -[[solution]] -=== Solution - -The solution for this problem is to use the DLQ feature in Spring Cloud Stream. -For the purposes of this discussion, let us assume that the following is our processor function. - -``` -@Bean -public Consumer processData() { - return s -> { - throw new RuntimeException(); - }; -} -``` - -This is a very trivial function that throws an exception for all the records that it processes, but you can take this function and extend it to any other similar situations. - -In order to send the records in error to a DLT, we need to provide the following configuration. - -``` -spring.cloud.stream: - bindings: - processData-in-0: - group: my-group - destination: input-topic - kafka: - bindings: - processData-in-0: - consumer: - enableDlq: true - dlqName: input-topic-dlq -``` - -In order to activate DLQ, the application must provide a group name. -Anonymous consumers cannot use the DLQ facilities. -We also need to enable DLQ by setting the `enableDLQ` property on the Kafka consumer binding to `true`. -Finally, we can optionally provide the DLT name by providing the `dlqName` on Kafka consumer binding, which otherwise default to `error.input-topic.my-group` in this case. - -Note that in the example consumer provided above, the type of the payload is `byte[]`. -By default, the DLQ producer in Kafka binder expects the payload of type `byte[]`. -If that is not the case, then we need to provide the configuration for proper serializer. -For example, let us re-write the consumer function as below: - -``` -@Bean -public Consumer processData() { - return s -> { - throw new RuntimeException(); - }; -} -``` - -Now, we need to tell Spring Cloud Stream, how we want to serialize the data when writing to the DLT. -Here is the modified configuration for this scenario: - -``` -spring.cloud.stream: - bindings: - processData-in-0: - group: my-group - destination: input-topic - kafka: - bindings: - processData-in-0: - consumer: - enableDlq: true - dlqName: input-topic-dlq - dlqProducerProperties: - configuration: - value.serializer: org.apache.kafka.common.serialization.StringSerializer - -``` - -[[dlq-with-advanced-retry-options]] -== DLQ with Advanced Retry Options - -[[problem-statement]] -=== Problem Statement - -This is similar to the recipe above, but as a developer I would like to configure the way retries are handled. - -[[solution]] -=== Solution - -If you followed the above recipe, then you get the default retry options built into the Kafka binder when the processing encounters an error. - -By default, the binder retires for a maximum of 3 attempts with a one second initial delay, 2.0 multiplier with each back off with a max delay of 10 seconds. -You can change all these configurations as below: - -``` -spring.cloud.stream.bindings.processData-in-0.consumer.maxAtttempts -spring.cloud.stream.bindings.processData-in-0.consumer.backOffInitialInterval -spring.cloud.stream.bindings.processData-in-0.consumer.backOffMultipler -spring.cloud.stream.bindings.processData-in-0.consumer.backOffMaxInterval -``` - -If you want, you can also provide a list of retryable exceptions by providing a map of boolean values. -For example, - -``` -spring.cloud.stream.bindings.processData-in-0.consumer.retryableExceptions.java.lang.IllegalStateException=true -spring.cloud.stream.bindings.processData-in-0.consumer.retryableExceptions.java.lang.IllegalArgumentException=false -``` - -By default, any exceptions not listed in the map above will be retried. -If that is not desired, then you can disable that by providing, - -``` -spring.cloud.stream.bindings.processData-in-0.consumer.defaultRetryable=false -``` - -You can also provide your own `RetryTemplate` and mark it as `@StreamRetryTemplate` which will be scanned and used by the binder. -This is useful when you want more sophisticated retry strategies and policies. - -If you have multiple `@StreamRetryTemplate` beans, then you can specify which one your binding wants by using the property, - -``` -spring.cloud.stream.bindings.processData-in-0.consumer.retry-template-name= -``` - -[[handling-deserialization-errors-with-dlq]] -== Handling Deserialization errors with DLQ - -[[problem-statement]] -=== Problem Statement - -I have a processor that encounters a deserialization exception in Kafka consumer. -I would expect that the Spring Cloud Stream DLQ mechanism will catch that scenario, but it does not. -How can I handle this? - -[[solution]] -=== Solution - -The normal DLQ mechanism offered by Spring Cloud Stream will not help when Kafka consumer throws an irrecoverable deserialization exception. -This is because, this exception happens even before the consumer's `poll()` method returns. -Spring for Apache Kafka project offers some great ways to help the binder with this situation. -Let us explore those. - -Assuming this is our function: - -``` -@Bean -public Consumer functionName() { - return s -> { - System.out.println(s); - }; -} -``` - -It is a trivial function that takes a `String` parameter. - -We want to bypass the message converters provided by Spring Cloud Stream and want to use native deserializers instead. -In the case of `String` types, it does not make much sense, but for more complex types like AVRO etc. you have to rely on external deserializers and therefore want to delegate the conversion to Kafka. - -Now when the consumer receives the data, let us assume that there is a bad record that causes a deserialization error, maybe someone passed an `Integer` instead of a `String` for example. -In that case, if you don't do something in the application, the exception will be propagated through the chain and your application will exit eventually. - -In order to handle this, you can add a `ListenerContainerCustomizer` `@Bean` that configures a `DefaultErrorHandler`. -This `DefaultErrorHandler` is configured with a `DeadLetterPublishingRecoverer`. -We also need to configure an `ErrorHandlingDeserializer` for the consumer. -That sounds like a lot of complex things, but in reality, it boils down to these 3 beans in this case. - -``` - @Bean - public ListenerContainerCustomizer> customizer(DefaultErrorHandler errorHandler) { - return (container, dest, group) -> { - container.setCommonErrorHandler(errorHandler); - }; - } -``` - -``` - @Bean - public DefaultErrorHandler errorHandler(DeadLetterPublishingRecoverer deadLetterPublishingRecoverer) { - return new DefaultErrorHandler(deadLetterPublishingRecoverer); - } -``` - -``` - @Bean - public DeadLetterPublishingRecoverer publisher(KafkaOperations bytesTemplate) { - return new DeadLetterPublishingRecoverer(bytesTemplate); - } -``` - -Let us analyze each of them. -The first one is the `ListenerContainerCustomizer` bean that takes a `DefaultErrorHandler`. -The container is now customized with that particular error handler. -You can learn more about container customization https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_advanced_consumer_configuration[here]. - -The second bean is the `DefaultErrorHandler` that is configured with a publishing to a `DLT`. -See https://docs.spring.io/spring-kafka/docs/current/reference/html/#seek-to-current[here] for more details on `DefaultErrorHandler`. - -The third bean is the `DeadLetterPublishingRecoverer` that is ultimately responsible for sending to the `DLT`. -By default, the `DLT` topic is named as the ORIGINAL_TOPIC_NAME.DLT. -You can change that though. -See the https://docs.spring.io/spring-kafka/docs/current/reference/html/#dead-letters[docs] for more details. - - -We also need to configure an https://docs.spring.io/spring-kafka/docs/current/reference/html/#error-handling-deserializer[ErrorHandlingDeserializer] through application config. - -The `ErrorHandlingDeserializer` delegates to the actual deserializer. -In case of errors, it sets key/value of the record to be null and includes the raw bytes of the message. -It then sets the exception in a header and passes this record to the listener, which then calls the registered error handler. - -Following is the configuration required: - -``` -spring.cloud.stream: - function: - definition: functionName - bindings: - functionName-in-0: - group: group-name - destination: input-topic - consumer: - use-native-decoding: true - kafka: - bindings: - functionName-in-0: - consumer: - enableDlq: true - dlqName: dlq-topic - dlqProducerProperties: - configuration: - value.serializer: org.apache.kafka.common.serialization.StringSerializer - configuration: - value.deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer - spring.deserializer.value.delegate.class: org.apache.kafka.common.serialization.StringDeserializer -``` - -We are providing the `ErrorHandlingDeserializer` through the `configuration` property on the binding. -We are also indicating that the actual deserializer to delegate is the `StringDeserializer`. - -Keep in mind that none of the dlq properties above are relevant for the discussions in this recipe. -They are purely meant for addressing any application level errors only. - -[[basic-offset-management-in-kafka-binder]] -== Basic offset management in Kafka binder - -[[problem-statement]] -=== Problem Statement - -I want to write a Spring Cloud Stream Kafka consumer application and not sure about how it manages Kafka consumer offsets. -Can you explain? - -[[solution]] -=== Solution - -We encourage you read the https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/current/reference/html/spring-cloud-stream-binder-kafka.html#reset-offsets[docs] section on this to get a thorough understanding on it. - -Here is it in a gist: - -Kafka supports two types of offsets to start with by default - `earliest` and `latest`. -Their semantics are self-explanatory from their names. - -Assuming you are running the consumer for the first time. -If you miss the group.id in your Spring Cloud Stream application, then it becomes an anonymous consumer. -Whenever, you have an anonymous consumer, in that case, Spring Cloud Stream application by default will start from the `latest` available offset in the topic partition. -On the other hand, if you explicitly specify a group.id, then by default, the Spring Cloud Stream application will start from the `earliest` available offset in the topic partition. - -In both cases above (consumers with explicit groups and anonymous groups), the starting offset can be switched around by using the property `spring.cloud.stream.kafka.bindings..consumer.startOffset` and setting it to either `earliest` or `latest`. - -Now, assume that you already ran the consumer before and now starting it again. -In this case, the starting offset semantics in the above case do not apply as the consumer finds an already committed offset for the consumer group (In the case of an anonymous consumer, although the application does not provide a group.id, the binder will auto generate one for you). -It simply picks up from the last committed offset onward. -This is true, even when you have a `startOffset` value provided. - -However, you can override the default behavior where the consumer starts from the last committed offset by using the `resetOffsets` property. -In order to do that, set the property `spring.cloud.stream.kafka.bindings..consumer.resetOffsets` to `true` (which is `false` by default). -Then make sure you provide the `startOffset` value (either `earliest` or `latest`). -When you do that and then start the consumer application, each time you start, it starts as if this is starting for the first time and ignore any committed offsets for the partition. - -[[seeking-to-arbitrary-offsets-in-kafka]] -== Seeking to arbitrary offsets in Kafka - -[[problem-statement]] -=== Problem Statement - -Using Kafka binder, I know that it can set the offset to either `earliest` or `latest`, but I have a requirement to seek the offset to something in the middle, an arbitrary offset. -Is there a way to achieve this using Spring Cloud Stream Kafka binder? - -[[solution]] -=== Solution - -Previously we saw how Kafka binder allows you to tackle basic offset management. -By default, the binder does not allow you to rewind to an arbitrary offset, at least through the mechanism we saw in that recipe. -However, there are some low-level strategies that the binder provides to achieve this use case. -Let's explore them. - -First of all, when you want to reset to an arbitrary offset other than `earliest` or `latest`, make sure to leave the `resetOffsets` configuration to its defaults, which is `false`. -Then you have to provide a custom bean of type `KafkaBindingRebalanceListener`, which will be injected into all consumer bindings. -It is an interface that comes with a few default methods, but here is the method that we are interested in: - -``` -/** - * Invoked when partitions are initially assigned or after a rebalance. Applications - * might only want to perform seek operations on an initial assignment. While the - * 'initial' argument is true for each thread (when concurrency is greater than 1), - * implementations should keep track of exactly which partitions have been sought. - * There is a race in that a rebalance could occur during startup and so a topic/ - * partition that has been sought on one thread may be re-assigned to another - * thread and you may not wish to re-seek it at that time. - * @param bindingName the name of the binding. - * @param consumer the consumer. - * @param partitions the partitions. - * @param initial true if this is the initial assignment on the current thread. - */ - default void onPartitionsAssigned(String bindingName, Consumer consumer, - Collection partitions, boolean initial) { - // do nothing - } -``` - -Let us look at the details. - -In essence, this method will be invoked each time during the initial assignment for a topic partition or after a rebalance. -For better illustration, let us assume that our topic is `foo` and it has 4 partitions. -Initially, we are only starting a single consumer in the group and this consumer will consume from all partitions. -When the consumer starts for the first time, all 4 partitions are getting initially assigned. -However, we do not want to start the partitions to consume at the defaults (`earliest` since we define a group), rather for each partition, we want them to consume after seeking to arbitrary offsets. -Imagine that you have a business case to consume from certain offsets as below. - -``` -Partition start offset - -0 1000 -1 2000 -2 2000 -3 1000 -``` - -This could be achieved by implementing the above method as below. - -``` - -@Override -public void onPartitionsAssigned(String bindingName, Consumer consumer, Collection partitions, boolean initial) { - - Map topicPartitionOffset = new HashMap<>(); - topicPartitionOffset.put(new TopicPartition("foo", 0), 1000L); - topicPartitionOffset.put(new TopicPartition("foo", 1), 2000L); - topicPartitionOffset.put(new TopicPartition("foo", 2), 2000L); - topicPartitionOffset.put(new TopicPartition("foo", 3), 1000L); - - if (initial) { - partitions.forEach(tp -> { - if (topicPartitionOffset.containsKey(tp)) { - final Long offset = topicPartitionOffset.get(tp); - try { - consumer.seek(tp, offset); - } - catch (Exception e) { - // Handle exceptions carefully. - } - } - }); - } -} -``` - -This is just a rudimentary implementation. -Real world use cases are much more complex than this and you need to adjust accordingly, but this certainly gives you a basic sketch. -When consumer `seek` fails, it may throw some runtime exceptions and you need to decide what to do in those cases. - -[[what-if-we-start-a-second-consumer-with-the-same-group-id?]] -=== What if we start a second consumer with the same group id? - -When we add a second consumer, a rebalance will occur and some partitions will be moved around. -Let's say that the new consumer gets partitions `2` and `3`. -When this new Spring Cloud Stream consumer calls this `onPartitionsAssigned` method, it will see that this is the initial assignment for partition `2` and `3` on this consumer. -Therefore, it will do the seek operation because of the conditional check on the `initial` argument. -In the case of the first consumer, it now only has partitions `0` and `1` -However, for this consumer it was simply a rebalance event and not considered as an intial assignment. -Thus, it will not re-seek to the given offsets because of the conditional check on the `initial` argument. - -[[how-do-i-manually-acknowledge-using-kafka-binder?]] -== How do I manually acknowledge using Kafka binder? - -[[problem-statement]] -=== Problem Statement - -Using Kafka binder, I want to manually acknowledge messages in my consumer. -How do I do that? - -[[solution]] -=== Solution - -By default, Kafka binder delegates to the default commit settings in Spring for Apache Kafka project. -The default `ackMode` in Spring Kafka is `batch`. -See https://docs.spring.io/spring-kafka/docs/current/reference/html/#committing-offsets[here] for more details on that. - -There are situations in which you want to disable this default commit behavior and rely on manual commits. -Following steps allow you to do that. - -Set the property `spring.cloud.stream.kafka.bindings..consumer.ackMode` to either `MANUAL` or `MANUAL_IMMEDIATE`. -When it is set like that, then there will be a header called `kafka_acknowledgment` (from `KafkaHeaders.ACKNOWLEDGMENT`) present in the message received by the consumer method. - -For example, imagine this as your consumer method. - -``` -@Bean -public Consumer> myConsumer() { - return msg -> { - Acknowledgment acknowledgment = message.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class); - if (acknowledgment != null) { - System.out.println("Acknowledgment provided"); - acknowledgment.acknowledge(); - } - }; -} -``` - -Then you set the property `spring.cloud.stream.kafka.bindings.myConsumer-in-0.consumer.ackMode` to `MANUAL` or `MANUAL_IMMEDIATE`. - -[[how-do-i-override-the-default-binding-names-in-spring-cloud-stream?]] -== How do I override the default binding names in Spring Cloud Stream? - -[[problem-statement]] -=== Problem Statement - -Spring Cloud Stream creates default bindings based on the function definition and signature, but how do I override these to more domain friendly names? - -[[solution]] -=== Solution - -Assume that following is your function signature. - -``` -@Bean -public Function uppercase(){ -... -} -``` - -By default, Spring Cloud Stream will create the bindings as below. - -1. uppercase-in-0 -2. uppercase-out-0 - -You can override these bindings to something by using the following properties. - -``` -spring.cloud.stream.function.bindings.uppercase-in-0=my-transformer-in -spring.cloud.stream.function.bindings.uppercase-out-0=my-transformer-out -``` - -After this, all binding properties must be made on the new names, `my-transformer-in` and `my-transformer-out`. - -Here is another example with Kafka Streams and multiple inputs. - -``` -@Bean -public BiFunction, KTable, KStream> processOrder() { -... -} -``` - -By default, Spring Cloud Stream will create three different binding names for this function. - -1. processOrder-in-0 -2. processOrder-in-1 -3. processOrder-out-0 - -You have to use these binding names each time you want to set some configuration on these bindings. -You don't like that, and you want to use more domain-friendly and readable binding names, for example, something like. - -1. orders -2. accounts -3. enrichedOrders - -You can easily do that by simply setting these three properties - -1. spring.cloud.stream.function.bindings.processOrder-in-0=orders -2. spring.cloud.stream.function.bindings.processOrder-in-1=accounts -3. spring.cloud.stream.function.bindings.processOrder-out-0=enrichedOrders - -Once you do that, it overrides the default binding names and any properties that you want to set on them must be on these new binding names. - -[[how-do-i-send-a-message-key-as-part-of-my-record?]] -== How do I send a message key as part of my record? - -[[problem-statement]] -=== Problem Statement - -I need to send a key along with the payload of the record, is there a way to do that in Spring Cloud Stream? - -[[solution]] -=== Solution - -It is often necessary that you want to send associative data structure like a map as the record with a key and value. -Spring Cloud Stream allows you to do that in a straightforward manner. -Following is a basic blueprint for doing this, but you may want to adapt it to your paricular use case. - -Here is sample producer method (aka `Supplier`). - -``` -@Bean -public Supplier> supplier() { - return () -> MessageBuilder.withPayload("foo").setHeader(KafkaHeaders.MESSAGE_KEY, "my-foo").build(); -} -``` - -This is a trivial function that sends a message with a `String` payload, but also with a key. -Note that we set the key as a message header using `KafkaHeaders.MESSAGE_KEY`. - -If you want to change the key from the default `kafka_messageKey`, then in the configuration, we need to specify this property: - -``` -spring.cloud.stream.kafka.bindings.supplier-out-0.producer.messageKeyExpression=headers['my-special-key'] -``` - -Please note that we use the binding name `supplier-out-0` since that is our function name, please update accordingly. - -Then, we use this new key when we produce the message. - -[[how-do-i-use-native-serializer-and-deserializer-instead-of-message-conversion-done-by-spring-cloud-stream?]] -== How do I use native serializer and deserializer instead of message conversion done by Spring Cloud Stream? - -[[problem-statement]] -=== Problem Statement - -Instead of using the message converters in Spring Cloud Stream, I want to use native Serializer and Deserializer in Kafka. -By default, Spring Cloud Stream takes care of this conversion using its internal built-in message converters. -How can I bypass this and delegate the responsibility to Kafka? - -[[solution]] -=== Solution - -This is really easy to do. - -All you have to do is to provide the following property to enable native serialization. - -``` -spring.cloud.stream.kafka.bindings..producer.useNativeEncoding: true -``` - -Then, you need to also set the serializers. -There are a couple of ways to do this. - -``` -spring.cloud.stream.kafka.bindings..producer.configuration.key.serializer: org.apache.kafka.common.serialization.StringSerializer -spring.cloud.stream.kafka.bindings..producer.configuration.value.serializer: org.apache.kafka.common.serialization.StringSerializer -``` - -or using the binder configuration. - -``` -spring.cloud.stream.kafka.binder.configuration.key.serializer: org.apache.kafka.common.serialization.StringSerializer -spring.cloud.stream.kafka.binder.configuration.value.serializer: org.apache.kafka.common.serialization.StringSerializer -``` - -When using the binder way, it is applied against all the bindings whereas setting them at the bindings are per binding. - -On the deserializing side, you just need to provide the deserializers as configuration. - -For example, - -``` -spring.cloud.stream.kafka.bindings..consumer.configuration.key.deserializer: org.apache.kafka.common.serialization.StringDeserializer -spring.cloud.stream.kafka.bindings..producer.configuration.value.deserializer: org.apache.kafka.common.serialization.StringDeserializer -``` - -You can also set them at the binder level. - -There is an optional property that you can set to force native decoding. - -``` -spring.cloud.stream.kafka.bindings..consumer.useNativeDecoding: true -``` - -However, in the case of Kafka binder, this is unnecessary, as by the time it reaches the binder, Kafka already deserializes them using the configured deserializers. - -[[explain-how-offset-resetting-work-in-kafka-streams-binder]] -== Explain how offset resetting work in Kafka Streams binder - -[[problem-statement]] -=== Problem Statement - -By default, Kafka Streams binder always starts from the earliest offset for a new consumer. -Sometimes, it is beneficial or required by the application to start from the latest offset. -Kafka Streams binder allows you to do that. - -[[solution]] -=== Solution - -Before we look at the solution, let us look at the following scenario. - -``` -@Bean -public BiConsumer, KTable> myBiConsumer{ - (s, t) -> s.join(t, ...) - ... -} -``` - -We have a `BiConsumer` bean that requires two input bindings. -In this case, the first binding is for a `KStream` and the second one is for a `KTable`. -When running this application for the first time, by default, both bindings start from the `earliest` offset. -What about I want to start from the `latest` offset due to some requirements? -You can do this by enabling the following properties. - -``` -spring.cloud.stream.kafka.streams.bindings.myBiConsumer-in-0.consumer.startOffset: latest -spring.cloud.stream.kafka.streams.bindings.myBiConsumer-in-1.consumer.startOffset: latest -``` - -If you want only one binding to start from the `latest` offset and the other to consumer from the default `earliest`, then leave the latter binding out from the configuration. - -Keep in mind that, once there are committed offsets, these setting are *not* honored and the committed offsets take precedence. - -[[keeping-track-of-successful-sending-of-records-producing-in-kafka]] -== Keeping track of successful sending of records (producing) in Kafka - -[[problem-statement]] -=== Problem Statement - -I have a Kafka producer application and I want to keep track of all my successful sendings. - -[[solution]] -=== Solution - -Let us assume that we have this following supplier in the application. - -``` -@Bean - public Supplier> supplier() { - return () -> MessageBuilder.withPayload("foo").setHeader(KafkaHeaders.MESSAGE_KEY, "my-foo").build(); - } -``` - -Then, we need to define a new `MessageChannel` bean to capture all the successful send information. - -``` -@Bean - public MessageChannel fooRecordChannel() { - return new DirectChannel(); - } -``` - -Next, define this property in the application configuration to provide the bean name for the `recordMetadataChannel`. - -``` -spring.cloud.stream.kafka.bindings.supplier-out-0.producer.recordMetadataChannel: fooRecordChannel -``` - -At this point, successful sent information will be sent to the `fooRecordChannel`. - -You can write an `IntegrationFlow` as below to see the information. - -``` -@Bean -public IntegrationFlow integrationFlow() { - return f -> f.channel("fooRecordChannel") - .handle((payload, messageHeaders) -> payload); -} -``` - -In the `handle` method, the payload is what got sent to Kafka and the message headers contain a special key called `kafka_recordMetadata`. -Its value is a `RecordMetadata` that contains information about topic partition, current offset etc. - -[[adding-custom-header-mapper-in-kafka]] -== Adding custom header mapper in Kafka - -[[problem-statement]] -=== Problem Statement - -I have a Kafka producer application that sets some headers, but they are missing in the consumer application. Why is that? - -[[solution]] -=== Solution - -Under normal circumstances, this should be fine. - -Imagine, you have the following producer. - -``` -@Bean -public Supplier> supply() { - return () -> MessageBuilder.withPayload("foo").setHeader("foo", "bar").build(); -} -``` - -On the consumer side, you should still see the header "foo", and the following should not give you any issues. - -``` -@Bean -public Consumer> consume() { - return s -> { - final String foo = (String)s.getHeaders().get("foo"); - System.out.println(foo); - }; -} -``` - -If you provide a https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/3.1.3/reference/html/spring-cloud-stream-binder-kafka.html#_kafka_binder_properties[custom header mapper] in the application, then this won't work. -Let's say you have an empty `KafkaHeaderMapper` in the application. - -``` -@Bean -public KafkaHeaderMapper kafkaBinderHeaderMapper() { - return new KafkaHeaderMapper() { - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - - } - - @Override - public void toHeaders(Headers source, Map target) { - - } - }; -} -``` - -If that is your implementation, then you will miss the `foo` header on the consumer. -Chances are that, you may have some logic inside those `KafkaHeaderMapper` methods. -You need the following to populate the `foo` header. - -``` -@Bean -public KafkaHeaderMapper kafkaBinderHeaderMapper() { - return new KafkaHeaderMapper() { - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - final String foo = (String) headers.get("foo"); - target.add("foo", foo.getBytes()); - } - - @Override - public void toHeaders(Headers source, Map target) { - final Header foo = source.lastHeader("foo"); - target.put("foo", new String(foo.value())); - } - } -``` - -That will properly populate the `foo` header from the producer to consumer. - -[[special-note-on-the-id-header]] -=== Special note on the id header - -In Spring Cloud Stream, the `id` header is a special header, but some applications may want to have special custom id headers - something like `custom-id` or `ID` or `Id`. -The first one (`custom-id`) will propagate without any custom header mapper from producer to consumer. -However, if you produce with a variant of the framework reserved `id` header - such as `ID`, `Id`, `iD` etc. then you will run into issues with the internals of the framework. -See this https://stackoverflow.com/questions/68412600/change-the-behaviour-in-spring-cloud-stream-make-header-matcher-case-sensitive[StackOverflow thread] fore more context on this use case. -In that case, you must use a custom `KafkaHeaderMapper` to map the case-sensitive id header. -For example, let's say you have the following producer. - -``` -@Bean -public Supplier> supply() { - return () -> MessageBuilder.withPayload("foo").setHeader("Id", "my-id").build(); -} -``` - -The header `Id` above will be gone from the consuming side as it clashes with the framework `id` header. -You can provide a custom `KafkaHeaderMapper` to solve this issue. - -``` -@Bean -public KafkaHeaderMapper kafkaBinderHeaderMapper1() { - return new KafkaHeaderMapper() { - @Override - public void fromHeaders(MessageHeaders headers, Headers target) { - final String myId = (String) headers.get("Id"); - target.add("Id", myId.getBytes()); - } - - @Override - public void toHeaders(Headers source, Map target) { - final Header Id = source.lastHeader("Id"); - target.put("Id", new String(Id.value())); - } - }; -} -``` - -By doing this, both `id` and `Id` headers will be available from the producer to the consumer side. - -[[producing-to-multiple-topics-in-transaction]] -== Producing to multiple topics in transaction - -[[problem-statement]] -=== Problem Statement - -How do I produce transactional messages to multiple Kafka topics? - -For more context, see this https://stackoverflow.com/questions/68928091/dlq-bounded-retry-and-eos-when-producing-to-multiple-topics-using-spring-cloud[StackOverflow question]. - -[[solution]] -=== Solution - -Use transactional support in Kafka binder for transactions and then provide an `AfterRollbackProcessor`. -In order to produce to multiple topics, use `StreamBridge` API. - -Below are the code snippets for this: - -``` -@Autowired -StreamBridge bridge; - -@Bean -Consumer input() { - return str -> { - System.out.println(str); - this.bridge.send("left", str.toUpperCase()); - this.bridge.send("right", str.toLowerCase()); - if (str.equals("Fail")) { - throw new RuntimeException("test"); - } - }; -} - -@Bean -ListenerContainerCustomizer> customizer(BinderFactory binders) { - return (container, dest, group) -> { - ProducerFactory pf = ((KafkaMessageChannelBinder) binders.getBinder(null, - MessageChannel.class)).getTransactionalProducerFactory(); - KafkaTemplate template = new KafkaTemplate<>(pf); - DefaultAfterRollbackProcessor rollbackProcessor = rollbackProcessor(template); - container.setAfterRollbackProcessor(rollbackProcessor); - }; -} - -DefaultAfterRollbackProcessor rollbackProcessor(KafkaTemplate template) { - return new DefaultAfterRollbackProcessor<>( - new DeadLetterPublishingRecoverer(template), new FixedBackOff(2000L, 2L), template, true); -} - -``` - -[[required-configuration]] -=== Required Configuration - -``` -spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix: tx- -spring.cloud.stream.kafka.binder.required-acks=all -spring.cloud.stream.bindings.input-in-0.group=foo -spring.cloud.stream.bindings.input-in-0.destination=input -spring.cloud.stream.bindings.left.destination=left -spring.cloud.stream.bindings.right.destination=right - -spring.cloud.stream.kafka.bindings.input-in-0.consumer.maxAttempts=1 -``` - -in order to test, you can use the following: - -``` -@Bean -public ApplicationRunner runner(KafkaTemplate template) { - return args -> { - System.in.read(); - template.send("input", "Fail".getBytes()); - template.send("input", "Good".getBytes()); - }; -} -``` - -Some important notes: - -Please ensure that you don't have any DLQ settings on the application configuration as we manually configure DLT (By default it will be published to a topic named `input.DLT` based on the initial consumer function). -Also, reset the `maxAttempts` on consumer binding to `1` in order to avoid retries by the binder. -It will be max tried a total of 3 in the example above (initial try + the 2 attempts in the `FixedBackoff`). - -See the https://stackoverflow.com/questions/68928091/dlq-bounded-retry-and-eos-when-producing-to-multiple-topics-using-spring-cloud[StackOverflow thread] for more details on how to test this code. -If you are using Spring Cloud Stream to test it by adding more consumer functions, make sure to set the `isolation-level` on the consumer binding to `read-committed`. - -This https://stackoverflow.com/questions/68941306/spring-cloud-stream-database-transaction-does-not-roll-back[StackOverflow thread] is also related to this discussion. - -[[pitfalls-to-avoid-when-running-multiple-pollable-consumers]] -== Pitfalls to avoid when running multiple pollable consumers - -[[problem-statement]] -=== Problem Statement - -How can I run multiple instances of the pollable consumers and generate unique `client.id` for each instance? - -[[solution]] -=== Solution - -Assuming that I have the following definition: - -``` -spring.cloud.stream.pollable-source: foo -spring.cloud.stream.bindings.foo-in-0.group: my-group -``` - -When running the application, the Kafka consumer generates a client.id (something like `consumer-my-group-1`). -For each instance of the application that is running, this `client.id` will be the same, causing unexpected issues. - -In order to fix this, you can add the following property on each instance of the application: - -``` -spring.cloud.stream.kafka.bindings.foo-in-0.consumer.configuration.client.id=${client.id} -``` - -See this https://github.com/spring-cloud/spring-cloud-stream-binder-kafka/issues/1139[GitHub issue] for more details. diff --git a/docs/modules/ROOT/pages/kafka/spring-cloud-stream-binder-kafka.adoc b/docs/modules/ROOT/pages/kafka/spring-cloud-stream-binder-kafka.adoc deleted file mode 100644 index d18bfc95a..000000000 --- a/docs/modules/ROOT/pages/kafka/spring-cloud-stream-binder-kafka.adoc +++ /dev/null @@ -1,40 +0,0 @@ -:github-tag: master -:github-repo: spring-cloud/spring-cloud-stream-binder-kafka -:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} -:github-code: https://github.com/{github-repo}/tree/{github-tag} -:nofooter: -:sectlinks: true - - -[[spring-cloud-stream-binder-kafka-reference]] -= Spring Cloud Stream Kafka Binder Reference Guide -Sabby Anandan, Marius Bogoevici, Eric Bottard, Mark Fisher, Ilayaperumal Gopinathan, Gunnar Hillert, Mark Pollack, Patrick Peralta, Glenn Renfro, Thomas Risberg, Dave Syer, David Turanski, Janne Valkealahti, Benjamin Klein, Henryk Konsek, Gary Russell, Arnaud JardinΓ©, Soby Chacko -:doctype: book -:source-highlighter: prettify -:numbered: -:icons: font -:hide-uri-scheme: -:spring-cloud-stream-binder-kafka-repo: snapshot -:github-tag: master -:spring-cloud-stream-binder-kafka-docs-version: current -:spring-cloud-stream-binder-kafka-docs: https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/{spring-cloud-stream-binder-kafka-docs-version}/reference -:spring-cloud-stream-binder-kafka-docs-current: https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/current-SNAPSHOT/reference/html/ -:github-repo: spring-cloud/spring-cloud-stream-binder-kafka -:github-raw: https://raw.github.com/{github-repo}/{github-tag} -:github-code: https://github.com/{github-repo}/tree/{github-tag} -:github-wiki: https://github.com/{github-repo}/wiki -:github-master-code: https://github.com/{github-repo}/tree/master -:sc-ext: java -// ====================================================================================== - - -*{project-version}* - - -[[reference-guide]] -= Reference Guide - - - - - diff --git a/docs/modules/ROOT/pages/kafka/streams/accessing-metrics.adoc b/docs/modules/ROOT/pages/kafka/streams/accessing-metrics.adoc deleted file mode 100644 index 48f313020..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/accessing-metrics.adoc +++ /dev/null @@ -1,12 +0,0 @@ -[[accessing-kafka-streams-metrics]] -= Accessing Kafka Streams Metrics -:page-section-summary-toc: 1 - -Spring Cloud Stream Kafka Streams binder provides Kafka Streams metrics which can be exported through a Micrometer `MeterRegistry`. - -For Spring Boot version 2.2.x, the metrics support is provided through a custom Micrometer metrics implementation by the binder. -For Spring Boot version 2.3.x, the Kafka Streams metrics support is provided natively through Micrometer. - -When accessing metrics through the Boot actuator endpoint, make sure to add `metrics` to the property `management.endpoints.web.exposure.include`. -Then you can access `/acutator/metrics` to get a list of all the available metrics, which then can be individually accessed through the same URI (`/actuator/metrics/`). - diff --git a/docs/modules/ROOT/pages/kafka/streams/ancillaries-to-the-programming-model.adoc b/docs/modules/ROOT/pages/kafka/streams/ancillaries-to-the-programming-model.adoc deleted file mode 100644 index 6ba426930..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/ancillaries-to-the-programming-model.adoc +++ /dev/null @@ -1,143 +0,0 @@ -[[ancillaries-to-the-programming-model]] -= Ancillaries to the programming model - -[[multiple-kafka-streams-processors-within-a-single-application]] -== Multiple Kafka Streams processors within a single application - -Binder allows to have multiple Kafka Streams processors within a single Spring Cloud Stream application. -You can have an application as below. - -``` -@Bean -public java.util.function.Function, KStream> process() { - ... -} - -@Bean -public java.util.function.Consumer> anotherProcess() { - ... -} - -@Bean -public java.util.function.BiFunction, KTable, KStream> yetAnotherProcess() { - ... -} - -``` - -In this case, the binder will create 3 separate Kafka Streams objects with different application ID's (more on this below). -However, if you have more than one processor in the application, you have to tell Spring Cloud Stream, which functions need to be activated. -Here is how you activate the functions. - -`spring.cloud.function.definition: process;anotherProcess;yetAnotherProcess` - -If you want certain functions to be not activated right away, you can remove that from this list. - -This is also true when you have a single Kafka Streams processor and other types of `Function` beans in the same application that is handled through a different binder (for e.g., a function bean that is based on the regular Kafka Message Channel binder) - -[[kafka-streams-application-id]] -== Kafka Streams Application ID - -Application id is a mandatory property that you need to provide for a Kafka Streams application. -Spring Cloud Stream Kafka Streams binder allows you to configure this application id in multiple ways. - -If you only have one single processor in the application, then you can set this at the binder level using the following property: - -`spring.cloud.stream.kafka.streams.binder.applicationId`. - -As a convenience, if you only have a single processor, you can also use `spring.application.name` as the property to delegate the application id. - -If you have multiple Kafka Streams processors in the application, then you need to set the application id per processor. -In the case of the functional model, you can attach it to each function as a property. - -For e.g. imagine that you have the following functions. - -``` -@Bean -public java.util.function.Consumer> process() { - ... -} -``` - -and - -``` -@Bean -public java.util.function.Consumer> anotherProcess() { - ... -} -``` - -Then you can set the application id for each, using the following binder level properties. - -`spring.cloud.stream.kafka.streams.binder.functions.process.applicationId` - -and - -`spring.cloud.stream.kafka.streams.binder.functions.anotherProcess.applicationId` - -For function based model also, this approach of setting application id at the binding level will work. -However, setting per function at the binder level as we have seen above is much easier if you are using the functional model. - -For production deployments, it is highly recommended to explicitly specify the application ID through configuration. -This is especially going to be very critical if you are auto scaling your application in which case you need to make sure that you are deploying each instance with the same application ID. - -If the application does not provide an application ID, then in that case the binder will auto generate a static application ID for you. -This is convenient in development scenarios as it avoids the need for explicitly providing the application ID. -The generated application ID in this manner will be static over application restarts. -In the case of functional model, the generated application ID will be the function bean name followed by the literal `applicationID`, for e.g `process-applicationID` if `process` if the function bean name. - -[[summary-of-setting-application-id]] -==== Summary of setting Application ID - -* By default, binder will auto generate the application ID per function methods. -* If you have a single processor, then you can use `spring.kafka.streams.applicationId`, `spring.application.name` or `spring.cloud.stream.kafka.streams.binder.applicationId`. -* If you have multiple processors, then application ID can be set per function using the property - `spring.cloud.stream.kafka.streams.binder.functions..applicationId`. - -[[overriding-the-default-binding-names-generated-by-the-binder-with-the-functional-style]] -== Overriding the default binding names generated by the binder with the functional style - -By default, the binder uses the strategy discussed above to generate the binding name when using the functional style, i.e. -|-[0..n], for e.g. process-in-0, process-out-0 etc. -If you want to override those binding names, you can do that by specifying the following properties. - -`spring.cloud.stream.function.bindings.`. Default binding name is the original binding name generated by the binder. - -For e.g. lets say, you have this function. - -[source] ----- -@Bean -public BiFunction, KTable, KStream> process() { -... -} ----- - -Binder will generate bindings with names, `process-in-0`, `process-in-1` and `process-out-0`. -Now, if you want to change them to something else completely, maybe more domain specific binding names, then you can do so as below. - -`spring.cloud.stream.function.bindings.process-in-0=users` - -`spring.cloud.stream.function.bindings.process-in-0=regions` - -and - -`spring.cloud.stream.function.bindings.process-out-0=clicks` - -After that, you must set all the binding level properties on these new binding names. - -Please keep in mind that with the functional programming model described above, adhering to the default binding names make sense in most situations. -The only reason you may still want to do this overriding is when you have larger number of configuration properties and you want to map the bindings to something more domain friendly. - -[[setting-up-bootstrap-server-configuration]] -== Setting up bootstrap server configuration - -When running Kafka Streams applications, you must provide the Kafka broker server information. -If you don't provide this information, the binder expects that you are running the broker at the default `localhost:9092`. -If that is not the case, then you need to override that. There are a couple of ways to do that. - -* Using the boot property - `spring.kafka.bootstrapServers` -* Binder level property - `spring.cloud.stream.kafka.streams.binder.brokers` - -When it comes to the binder level property, it doesn't matter if you use the broker property provided through the regular Kafka binder - `spring.cloud.stream.kafka.binder.brokers`. -Kafka Streams binder will first check if Kafka Streams binder specific broker property is set (`spring.cloud.stream.kafka.streams.binder.brokers`) and if not found, it looks for `spring.cloud.stream.kafka.binder.brokers`. - diff --git a/docs/modules/ROOT/pages/kafka/streams/binding-visualization-and-control-in-binder.adoc b/docs/modules/ROOT/pages/kafka/streams/binding-visualization-and-control-in-binder.adoc deleted file mode 100644 index d2823c565..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/binding-visualization-and-control-in-binder.adoc +++ /dev/null @@ -1,113 +0,0 @@ -[[binding-visualization-and-control-in-kafka-streams-binder]] -= Binding visualization and control in Kafka Streams binder - -Starting with version 3.1.2, Kafka Streams binder supports binding visualization and control. -The only two lifecycle phases supported are `STOPPED` and `STARTED`. -The lifecycle phases `PAUSED` and `RESUMED` are not available in Kafka Streams binder. - -In order to activate binding visualization and control, the application needs to include the following two dependencies. - -``` - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-web - -``` - -If you prefer using webflux, you can then include `spring-boot-starter-webflux` instead of the standard web dependency. - -In addition, you also need to set the following property: - -``` -management.endpoints.web.exposure.include=bindings -``` - -To illustrate this feature further, let us use the following application as a guide: - -``` -@SpringBootApplication -public class KafkaStreamsApplication { - - public static void main(String[] args) { - SpringApplication.run(KafkaStreamsApplication.class, args); - } - - @Bean - public Consumer> consumer() { - return s -> s.foreach((key, value) -> System.out.println(value)); - } - - @Bean - public Function, KStream> function() { - return ks -> ks; - } - -} -``` - -As we can see, the application has two Kafka Streams functions - one, a consumer and another a function. -The consumer binding is named by default as `consumer-in-0`. -Similarly, for the function, the input binding is `function-in-0` and the output binding is `function-out-0`. - -Once the application is started, we can find details about the bindings using the following bindings endpoint. - -``` - curl http://localhost:8080/actuator/bindings | jq . -[ - { - "bindingName": "consumer-in-0", - "name": "consumer-in-0", - "group": "consumer-applicationId", - "pausable": false, - "state": "running", - "paused": false, - "input": true, - "extendedInfo": {} - }, - { - "bindingName": "function-in-0", - "name": "function-in-0", - "group": "function-applicationId", - "pausable": false, - "state": "running", - "paused": false, - "input": true, - "extendedInfo": {} - }, - { - "bindingName": "function-out-0", - "name": "function-out-0", - "group": "function-applicationId", - "pausable": false, - "state": "running", - "paused": false, - "input": false, - "extendedInfo": {} - } -] -``` - -The details about all three bindings can be found above. - -Let us now stop the consumer-in-0 binding. - -``` -curl -d '{"state":"STOPPED"}' -H "Content-Type: application/json" -X POST http://localhost:8080/actuator/bindings/consumer-in-0 -``` - -At this point, no records will be received through this binding. - -Start the binding again. - -``` -curl -d '{"state":"STARTED"}' -H "Content-Type: application/json" -X POST http://localhost:8080/actuator/bindings/consumer-in-0 -``` - -When there are multiple bindings present on a single function, invoking these operations on any of those bindings will work. -This is because all the bindings on a single function are backed by the same `StreamsBuilderFactoryBean`. -Therefore, for the function above, either `function-in-0` or `function-out-0` will work. - diff --git a/docs/modules/ROOT/pages/kafka/streams/configuration-options.adoc b/docs/modules/ROOT/pages/kafka/streams/configuration-options.adoc deleted file mode 100644 index bec2274fa..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/configuration-options.adoc +++ /dev/null @@ -1,209 +0,0 @@ -[[configuration-options]] -= Configuration Options - -This section contains the configuration options used by the Kafka Streams binder. - -For common configuration options and properties pertaining to binder, refer to the xref:spring-cloud-stream/binding-properties.adoc[core documentation]. - -[[kafka-streams-binder-properties]] -== Kafka Streams Binder Properties - -The following properties are available at the binder level and must be prefixed with `spring.cloud.stream.kafka.streams.binder.` -Any Kafka binder provided properties re-used in Kafka Streams binder must be prefixed with `spring.cloud.stream.kafka.streams.binder` instead of `spring.cloud.stream.kafka.binder`. -The only exception to this rule is when defining the Kafka bootstrap server property in which case either prefix works. - -configuration:: -Map with a key/value pair containing properties pertaining to Apache Kafka Streams API. -This property must be prefixed with `spring.cloud.stream.kafka.streams.binder.`. -Following are some examples of using this property. - -[source] ----- -spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=org.apache.kafka.common.serialization.Serdes$StringSerde -spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=org.apache.kafka.common.serialization.Serdes$StringSerde -spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000 ----- - -For more information about all the properties that may go into streams configuration, see `StreamsConfig` JavaDocs in Apache Kafka Streams docs. -All configuration that you can set from `StreamsConfig` can be set through this. -When using this property, it is applicable against the entire application since this is a binder level property. -If you have more than processors in the application, all of them will acquire these properties. -In the case of properties like `application.id`, this will become problematic and therefore you have to carefully examine how the properties from `StreamsConfig` are mapped using this binder level `configuration` property. - -functions..applicationId:: -Applicable only for functional style processors. -This can be used for setting application ID per function in the application. -In the case of multiple functions, this is a handy way to set the application ID. - -functions..configuration:: -Applicable only for functional style processors. -Map with a key/value pair containing properties pertaining to Apache Kafka Streams API. -This is similar to the binder level `configuration` property describe above, but this level of `configuration` property is restricted only against the named function. -When you have multiple processors and you want to restrict access to the configuration based on particular functions, you might want to use this. -All `StreamsConfig` properties can be used here. - -brokers:: -Broker URL -+ -Default: `localhost` -zkNodes:: -Zookeeper URL -+ -Default: `localhost` - -deserializationExceptionHandler:: -Deserialization error handler type. -This handler is applied at the binder level and thus applied against all input binding in the application. -There is a way to control it in a more fine-grained way at the consumer binding level. -Possible values are - `logAndContinue`, `logAndFail`, `skipAndContinue` or `sendToDlq` -+ -Default: `logAndFail` - -applicationId:: -Convenient way to set the application.id for the Kafka Streams application globally at the binder level. -If the application contains multiple functions, then the application id should be set differently. -See above where setting the application id is discussed in detail. -+ -Default: application will generate a static application ID. See the application ID section for more details. - -stateStoreRetry.maxAttempts:: -Max attempts for trying to connect to a state store. -+ -Default: 1 - -stateStoreRetry.backoffPeriod:: -Backoff period when trying to connect to a state store on a retry. -+ -Default: 1000 ms - -consumerProperties:: -Arbitrary consumer properties at the binder level. - -producerProperties:: -Arbitrary producer properties at the binder level. - -includeStoppedProcessorsForHealthCheck:: -When bindings for processors are stopped through actuator, then this processor will not participate in the health check by default. -Set this property to `true` to enable health check for all processors including the ones that are currently stopped through bindings actuator endpoint. -+ -Default: false - -[[kafka-streams-producer-properties]] -== Kafka Streams Producer Properties - -The following properties are _only_ available for Kafka Streams producers and must be prefixed with `spring.cloud.stream.kafka.streams.bindings..producer.` -For convenience, if there are multiple output bindings and they all require a common value, that can be configured by using the prefix `spring.cloud.stream.kafka.streams.default.producer.`. - -keySerde:: -key serde to use -+ -Default: See the above discussion on message de/serialization - -valueSerde:: -value serde to use -+ -Default: See the above discussion on message de/serialization - -useNativeEncoding:: -flag to enable/disable native encoding -+ -Default: `true`. - -streamPartitionerBeanName:: -Custom outbound partitioner bean name to be used at the consumer. -Applications can provide custom `StreamPartitioner` as a Spring bean and the name of this bean can be provided to the producer to use instead of the default one. -+ -Default: See the discussion above on outbound partition support. - -producedAs:: -Custom name for the sink component to which the processor is producing to. -+ -Deafult: `none` (generated by Kafka Streams) - -[[kafka-streams-consumer-properties]] -== Kafka Streams Consumer Properties - -The following properties are available for Kafka Streams consumers and must be prefixed with `spring.cloud.stream.kafka.streams.bindings..consumer.` -For convenience, if there are multiple input bindings and they all require a common value, that can be configured by using the prefix `spring.cloud.stream.kafka.streams.default.consumer.`. - -applicationId:: -Setting application.id per input binding. -+ -Default: See above. - -keySerde:: -key serde to use -+ -Default: See the above discussion on message de/serialization - -valueSerde:: -value serde to use -+ -Default: See the above discussion on message de/serialization - -materializedAs:: -state store to materialize when using incoming KTable types -+ -Default: `none`. - -useNativeDecoding:: -flag to enable/disable native decoding -+ -Default: `true`. - -dlqName:: -DLQ topic name. -+ -Default: See above on the discussion of error handling and DLQ. - -startOffset:: -Offset to start from if there is no committed offset to consume from. -This is mostly used when the consumer is consuming from a topic for the first time. -Kafka Streams uses `earliest` as the default strategy and the binder uses the same default. -This can be overridden to `latest` using this property. -+ -Default: `earliest`. - -Note: Using `resetOffsets` on the consumer does not have any effect on Kafka Streams binder. -Unlike the message channel based binder, Kafka Streams binder does not seek to beginning or end on demand. - -deserializationExceptionHandler:: -Deserialization error handler type. -This handler is applied per consumer binding as opposed to the binder level property described before. -Possible values are - `logAndContinue`, `logAndFail`, `skipAndContinue` or `sendToDlq` -+ -Default: `logAndFail` - -timestampExtractorBeanName:: -Specific time stamp extractor bean name to be used at the consumer. -Applications can provide `TimestampExtractor` as a Spring bean and the name of this bean can be provided to the consumer to use instead of the default one. -+ -Default: See the discussion above on timestamp extractors. - -eventTypes:: -Comma separated list of supported event types for this binding. -+ -Default: `none` - -eventTypeHeaderKey:: -Event type header key on each incoming records through this binding. -+ -Default: `event_type` - -consumedAs:: -Custom name for the source component from which the processor is consuming from. -+ -Deafult: `none` (generated by Kafka Streams) - -[[special-note-on-concurrency]] -== Special note on concurrency - -In Kafka Streams, you can control of the number of threads a processor can create using the `num.stream.threads` property. -This, you can do using the various `configuration` options described above under binder, functions, producer or consumer level. -You can also use the `concurrency` property that core Spring Cloud Stream provides for this purpose. -When using this, you need to use it on the consumer. -When you have more than one input binding, set this on the first input binding. -For e.g. when setting `spring.cloud.stream.bindings.process-in-0.consumer.concurrency`, it will be translated as `num.stream.threads` by the binder. -If you have multiple processors and one processor defines binding level concurrency, but not the others, those ones with no binding level concurrency will default back to the binder wide property specified through -`spring.cloud.stream.kafka.streams.binder.configuration.num.stream.threads`. -If this binder configuration is not available, then the application will use the default set by Kafka Streams. diff --git a/docs/modules/ROOT/pages/kafka/streams/error-handling.adoc b/docs/modules/ROOT/pages/kafka/streams/error-handling.adoc deleted file mode 100644 index 0a20316c3..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/error-handling.adoc +++ /dev/null @@ -1,195 +0,0 @@ -[[error-handling]] -= Error Handling - -Apache Kafka Streams provides the capability for natively handling exceptions from deserialization errors. -For details on this support, please see https://cwiki.apache.org/confluence/display/KAFKA/KIP-161%3A+streams+deserialization+exception+handlers[this]. -Out of the box, Apache Kafka Streams provides two kinds of deserialization exception handlers - `LogAndContinueExceptionHandler` and `LogAndFailExceptionHandler`. -As the name indicates, the former will log the error and continue processing the next records and the latter will log the error and fail. `LogAndFailExceptionHandler` is the default deserialization exception handler. - -[[handling-deserialization-exceptions-in-the-binder]] -== Handling Deserialization Exceptions in the Binder - -Kafka Streams binder allows to specify the deserialization exception handlers above using the following property. - -[source] ----- -spring.cloud.stream.kafka.streams.binder.deserializationExceptionHandler: logAndContinue ----- - -or - -[source] ----- -spring.cloud.stream.kafka.streams.binder.deserializationExceptionHandler: logAndFail ----- - -In addition to the above two deserialization exception handlers, the binder also provides a third one for sending the erroneous records (poison pills) to a DLQ (dead letter queue) topic. -Here is how you enable this DLQ exception handler. - -[source] ----- -spring.cloud.stream.kafka.streams.binder.deserializationExceptionHandler: sendToDlq ----- - -When the above property is set, all the records in deserialization error are automatically sent to the DLQ topic. - -You can set the topic name where the DLQ messages are published as below. - -You can provide an implementation for `DlqDestinationResolver` which is a functional interface. -`DlqDestinationResolver` takes `ConsumerRecord` and the exception as inputs and then allows to specify a topic name as the output. -By gaining access to the Kafka `ConsumerRecord`, the header records can be introspected in the implementation of the `BiFunction`. - -Here is an example of providing an implementation for `DlqDestinationResolver`. - -[source] ----- -@Bean -public DlqDestinationResolver dlqDestinationResolver() { - return (rec, ex) -> { - if (rec.topic().equals("word1")) { - return "topic1-dlq"; - } - else { - return "topic2-dlq"; - } - }; -} ----- - -One important thing to keep in mind when providing an implementation for `DlqDestinationResolver` is that the provisioner in the binder will not auto create topics for the application. -This is because there is no way for the binder to infer the names of all the DLQ topics the implementation might send to. -Therefore, if you provide DLQ names using this strategy, it is the application's responsibility to ensure that those topics are created beforehand. - -If `DlqDestinationResolver` is present in the application as a bean, that takes higher precedence. -If you do not want to follow this approach and rather provide a static DLQ name using configuration, you can set the following property. - -[source] ----- -spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.dlqName: custom-dlq (Change the binding name accordingly) ----- - -If this is set, then the error records are sent to the topic `custom-dlq`. -If the application is not using either of the above strategies, then it will create a DLQ topic with the name `error..`. -For instance, if your binding's destination topic is `inputTopic` and the application ID is `process-applicationId`, then the default DLQ topic is `error.inputTopic.process-applicationId`. -It is always recommended to explicitly create a DLQ topic for each input binding if it is your intention to enable DLQ. - -[[dlq-per-input-consumer-binding]] -== DLQ per input consumer binding - -The property `spring.cloud.stream.kafka.streams.binder.deserializationExceptionHandler` is applicable for the entire application. -This implies that if there are multiple functions in the same application, this property is applied to all of them. -However, if you have multiple processors or multiple input bindings within a single processor, then you can use the finer-grained DLQ control that the binder provides per input consumer binding. - -If you have the following processor, - -``` -@Bean -public BiFunction, KTable, KStream> process() { -... -} -``` - -and you only want to enable DLQ on the first input binding and skipAndContinue on the second binding, then you can do so on the consumer as below. - -`spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.deserializationExceptionHandler: sendToDlq` -`spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.deserializationExceptionHandler: skipAndContinue` - -Setting deserialization exception handlers this way has a higher precedence than setting at the binder level. - -[[dlq-partitioning]] -== DLQ partitioning - -By default, records are published to the Dead-Letter topic using the same partition as the original record. -This means the Dead-Letter topic must have at least as many partitions as the original record. - -To change this behavior, add a `DlqPartitionFunction` implementation as a `@Bean` to the application context. -Only one such bean can be present. -The function is provided with the consumer group (which is the same as the application ID in most situations), the failed `ConsumerRecord` and the exception. -For example, if you always want to route to partition 0, you might use: - - -[source, java] ----- -@Bean -public DlqPartitionFunction partitionFunction() { - return (group, record, ex) -> 0; -} ----- - -NOTE: If you set a consumer binding's `dlqPartitions` property to 1 (and the binder's `minPartitionCount` is equal to `1`), there is no need to supply a `DlqPartitionFunction`; the framework will always use partition 0. -If you set a consumer binding's `dlqPartitions` property to a value greater than `1` (or the binder's `minPartitionCount` is greater than `1`), you **must** provide a `DlqPartitionFunction` bean, even if the partition count is the same as the original topic's. - - -A couple of things to keep in mind when using the exception handling feature in Kafka Streams binder. - -* The property `spring.cloud.stream.kafka.streams.binder.deserializationExceptionHandler` is applicable for the entire application. -This implies that if there are multiple functions in the same application, this property is applied to all of them. -* The exception handling for deserialization works consistently with native deserialization and framework provided message conversion. - -[[handling-production-exceptions-in-the-binder]] -== Handling Production Exceptions in the Binder - -Unlike the support for deserialization exception handlers as described above, the binder does not provide such first class mechanisms for handling production exceptions. -However, you still can configure production exception handlers using the `StreamsBuilderFactoryBean` customizer which you can find more details about, in a subsequent section below. - -[[runtime-error-handling]] -== Runtime Error Handling - -When it comes to handling errors from application code, i.e. from the business logic execution, it is usually up to the application to handle that. -Because, the Kafka Streams binder does not have a way to interfere with the application code. -However, to make things a bit easier for the application, the binder provides a convenient `DltAwareProcessor`, using which, you can dictate how you want to handle the application level errors. - -Consider the following code. - -``` -@Bean -public java.util.function.Function, KStream> process() { - return input -> input - .map(...); -} -``` - -If the business code inside your `map` call above throws an exception, it is your responsibility to handle that error. -This is where `DltAwareProcessor` becomes handy. -Let's say that you want to publish the failed record to a DLT, rather than handling it within the application. -Here is how you can do that. - -``` -@Bean -public java.util.function.Function, KStream> process(DltPublishingContext dltSenderContext) { - return input -> input - .process(() -> new DltAwareProcessor<>((k, v) -> { - throw new RuntimeException("error"); - }, "hello-dlt-1", dltPublishingContext)); -} -``` - -The business logic code from the original `map` call now has been moved as part of `KStream#process` method call, which takes a `ProcessorSupplier`. -We, then, pass in the custom `DltAwareProcessor,` which is capable to publishing to a DLT. -The constructor for `DltAwareProcessor` above takes three parameters - a `BiFunction` that takes the key and value of the input record and then the business logic operation as part of the `BiFunction` body, the DLT topic, and finally a `DltPublishingContext`. When the `BiFunction`'s lambda expression throws an exception, the `DltAwareProcessor` will send the input record to a DLT. The `DltPublishingContext` provides `DltAwareProcessor` the necessary publishing infrastructure beans. -The `DltPublishingContext` is autoconfigured by the binder, so that you can inject directly this into the application. - -If you want to provide a custom timestamp on the record that gets published to the DLT, then you can provide an optional fourth constructor argument which is a `Supplier`. -If this value is provided, then this `Supplier` is invoked each time `DltAwareProcessor` publishes to the DLT. - -If you do not want the binder to publish failed records to a DLT, then you can provide your own recoverer as a `BiConsumer`. -Assume a scenario, in which you do not want to send the record to the DLT, but simply log the message and move on. -For this, it is convenient, if we can override the recovery process in `DltAwareProcessor`. -Here is an example of how you do that. - -``` -@Bean -public java.util.function.Function, KStream> process() { - return input -> input - .process(() -> new DltAwareProcessor<>((k, v) -> { - throw new RuntimeException("error"); - }, - (record, exception) -> { - // log the message - })); -} -``` - -In this case, when the record fails, the `DltAwareProcessor`, instead of using its built-in recoverer which publishes to a DLT, uses the user provided recoverer which is a `BiConsumer` that takes the failed record and the exception thrown as arguments. -In this case also, you can provide an optional `Supplier` to dictate the timestamp used in the record passed in to the `BiConsumer` recoverer. - diff --git a/docs/modules/ROOT/pages/kafka/streams/event-type-based-routing-in-applications.adoc b/docs/modules/ROOT/pages/kafka/streams/event-type-based-routing-in-applications.adoc deleted file mode 100644 index b2cbe5af0..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/event-type-based-routing-in-applications.adoc +++ /dev/null @@ -1,43 +0,0 @@ -[[event-type-based-routing-in-kafka-streams-applications]] -= Event type based routing in Kafka Streams applications - -Routing functions available in regular message channel based binders are not supported in Kafka Streams binder. -However, Kafka Streams binder still provides routing capabilities through the event type record header on the inbound records. - -To enable routing based on event types, the application must provide the following property. - -`spring.cloud.stream.kafka.streams.bindings..consumer.eventTypes`. - -This can be a comma separated value. - -For example, lets assume we have this function: - -``` -@Bean -public Function, KStream> process() { - return input -> input; -} -``` - -Let us also assume that we only want the business logic in this function to be executed, if the incoming record has event types as `foo` or `bar`. -That can be expressed as below using the `eventTypes` property on the binding. - -`spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.eventTypes=foo,bar` - -Now, when the application runs, the binder checks each incoming records for the header `event_type` and see if it has value set as `foo` or `bar`. -If it does not find either of them, then the function execution will be skipped. - -By default, the binder expects the record header key to be `event_type`, but that can be changed per binding. -For instance, if we want to change the header key on this binding to `my_event` instead of the default, that can be changed as below. - -`spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.eventTypeHeaderKey=my_event`. - -When using the event routing feature in Kafkfa Streams binder, it uses the byte array `Serde` to deserialze all incoming records. -If the record headers match the event type, then only it uses the actual `Serde` to do a proper deserialization using either the configured or the inferred `Serde`. -This introduces issues if you set a deserialization exception handler on the binding as the expected deserialization only happens down the stack causing unexpected errors. -In order to address this issue, you can set the following property on the binding to force the binder to use the configured or inferred `Serde` instead of byte array `Serde`. - -`spring.cloud.stream.kafka.streams.bindings..consumer.useConfiguredSerdeWhenRoutingEvents` - -This way, the application can detect deserialization issues right away when using the event routing feature and can take appropriate handling decisions. - diff --git a/docs/modules/ROOT/pages/kafka/streams/health-indicator.adoc b/docs/modules/ROOT/pages/kafka/streams/health-indicator.adoc deleted file mode 100644 index 871d711c3..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/health-indicator.adoc +++ /dev/null @@ -1,34 +0,0 @@ -[[health-indicator]] -= Health Indicator - -The health indicator requires the dependency `spring-boot-starter-actuator`. For maven use: -[source,xml] ----- - - org.springframework.boot - spring-boot-starter-actuator - ----- - -Spring Cloud Stream Kafka Streams Binder provides a health indicator to check the state of the underlying streams threads. -Spring Cloud Stream defines a property `management.health.binders.enabled` to enable the health indicator. See the -https://docs.spring.io/spring-cloud-stream/docs/current/reference/htmlsingle/#_health_indicator[Spring Cloud Stream documentation]. - -The health indicator provides the following details for each stream thread's metadata: - -* Thread name -* Thread state: `CREATED`, `RUNNING`, `PARTITIONS_REVOKED`, `PARTITIONS_ASSIGNED`, `PENDING_SHUTDOWN` or `DEAD` -* Active tasks: task ID and partitions -* Standby tasks: task ID and partitions - -By default, only the global status is visible (`UP` or `DOWN`). To show the details, the property `management.endpoint.health.show-details` must be set to `ALWAYS` or `WHEN_AUTHORIZED`. -For more details about the health information, see the -https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html#production-ready-health[Spring Boot Actuator documentation]. - -NOTE: The status of the health indicator is `UP` if all the Kafka threads registered are in the `RUNNING` state. - -Since there are three individual binders in Kafka Streams binder (`KStream`, `KTable` and `GlobalKTable`), all of them will report the health status. -When enabling `show-details`, some of the information reported may be redundant. - -When there are multiple Kafka Streams processors present in the same application, then the health checks will be reported for all of them and will be categorized by the application ID of Kafka Streams. - diff --git a/docs/modules/ROOT/pages/kafka/streams/interactive-queries.adoc b/docs/modules/ROOT/pages/kafka/streams/interactive-queries.adoc deleted file mode 100644 index 9a97d4914..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/interactive-queries.adoc +++ /dev/null @@ -1,92 +0,0 @@ -[[interactive-queries]] -= Interactive Queries - -Kafka Streams binder API exposes a class called `InteractiveQueryService` to interactively query the state stores. -You can access this as a Spring bean in your application. An easy way to get access to this bean from your application is to `autowire` the bean. - -[source] ----- -@Autowired -private InteractiveQueryService interactiveQueryService; ----- - -Once you gain access to this bean, then you can query for the particular state-store that you are interested. See below. - -[source] ----- -ReadOnlyKeyValueStore keyValueStore = - interactiveQueryService.getQueryableStoreType("my-store", QueryableStoreTypes.keyValueStore()); ----- - -During the startup, the above method call to retrieve the store might fail. -For example, it might still be in the middle of initializing the state store. -In such cases, it will be useful to retry this operation. -Kafka Streams binder provides a simple retry mechanism to accommodate this. - -Following are the two properties that you can use to control this retrying. - -* spring.cloud.stream.kafka.streams.binder.stateStoreRetry.maxAttempts - Default is `1` . -* spring.cloud.stream.kafka.streams.binder.stateStoreRetry.backOffInterval - Default is `1000` milliseconds. - -If there are multiple instances of the kafka streams application running, then before you can query them interactively, you need to identify which application instance hosts the particular key that you are querying. -`InteractiveQueryService` API provides methods for identifying the host information. - -In order for this to work, you must configure the property `application.server` as below: - -[source] ----- -spring.cloud.stream.kafka.streams.binder.configuration.application.server: : ----- - -Here are some code snippets: - -[source] ----- -org.apache.kafka.streams.state.HostInfo hostInfo = interactiveQueryService.getHostInfo("store-name", - key, keySerializer); - -if (interactiveQueryService.getCurrentHostInfo().equals(hostInfo)) { - - //query from the store that is locally available -} -else { - //query from the remote host -} ----- - -For more information on these host finding methods, please see the Javadoc on the methods. -For these methods also, during startup, if the underlying KafkaStreams objects are not ready, they might throw exceptions. -The aforementioned retry properties are applicable for these methods as well. - -[[other-api-methods-available-through-the-interactivequeryservice]] -== Other API methods available through the InteractiveQueryService - -Use the following API method to retrieve the `KeyQueryMetadata` object associated with the combination of given store and key. - -``` -public KeyQueryMetadata getKeyQueryMetadata(String store, K key, Serializer serializer) -``` - -Use the following API method to retrieve the `KakfaStreams` object associated with the combination of given store and key. - -``` -public KafkaStreams getKafkaStreams(String store, K key, Serializer serializer) -``` - -[[customizing-store-query-parameters]] -== Customizing Store Query Parameters - -Sometimes it is necessary that you need to fine tune the store query parameters before querying the store through `InteractiveQueryService`. -For this purpose, starting with the `4.0.1` version of the binder, you can provide a bean for `StoreQueryParametersCustomizer` which is a functional interface with a `customize` method that takes a `StoreQueryParameter` as the argument. -Here is its method signature. - -``` -StoreQueryParameters customize(StoreQueryParameters storeQueryParameters); -``` - -Using this approach, applications can further customize the `StoreQueryParameters` such as enabling stale stores. - -When this bean is present in this application, `InteractiveQueryService` will call its `customize` method before querying the state store. - -NOTE: Keep in mind that, there must be a unique bean for `StoreQueryParametersCustomizer` available in the application. - diff --git a/docs/modules/ROOT/pages/kafka/streams/manually-starting-processors-selectively.adoc b/docs/modules/ROOT/pages/kafka/streams/manually-starting-processors-selectively.adoc deleted file mode 100644 index 3b19c7f98..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/manually-starting-processors-selectively.adoc +++ /dev/null @@ -1,73 +0,0 @@ -[[manually-starting-kafka-streams-processors-selectively]] -= Manually starting Kafka Streams processors selectively - -While the approach laid out above will unconditionally apply auto start `false` to all the Kafka Streams processors in the application through `StreamsBuilderFactoryManager`, it is often desirable that only individually selected Kafka Streams processors are not auto started. -For instance, let us assume that you have three different functions (processors) in your application and for one of the processors, you do not want to start it as part of the application startup. -Here is an example of such a situation. - -``` - -@Bean -public Function, KStream> process1() { - -} - -@Bean -public Consumer> process2() { - -} - -@Bean -public BiFunction, KTable, KStream> process3() { - -} - -``` - -In this scenario above, if you set `spring.kafka.streams.auto-startup` to `false`, then none of the processors will auto start during the application startup. -In that case, you have to programmatically start them as described above by calling `start()` on the underlying `StreamsBuilderFactoryManager`. -However, if we have a use case to selectively disable only one processor, then you have to set `auto-startup` on the individual binding for that processor. -Let us assume that we don't want our `process3` function to auto start. -This is a `BiFunction` with two input bindings - `process3-in-0` and `process3-in-1`. -In order to avoid auto start for this processor, you can pick any of these input bindings and set `auto-startup` on them. -It does not matter which binding you pick; if you wish, you can set `auto-startup` to `false` on both of them, but one will be sufficient. -Because they share the same factory bean, you don't have to set autoStartup to false on both bindings, but it probably makes sense to do so, for clarity. - -Here is the Spring Cloud Stream property that you can use to disable auto startup for this processor. - -``` -spring.cloud.stream.bindings.process3-in-0.consumer.auto-startup: false -``` - -or - -``` -spring.cloud.stream.bindings.process3-in-1.consumer.auto-startup: false -``` - -Then, you can manually start the processor either using the REST endpoint or using the `BindingsEndpoint` API as shown below. -For this, you need to ensure that you have the Spring Boot actuator dependency on the classpath. - -``` -curl -d '{"state":"STARTED"}' -H "Content-Type: application/json" -X POST http://localhost:8080/actuator/bindings/process3-in-0 -``` - -or - -``` -@Autowired -BindingsEndpoint endpoint; - -@Bean -public ApplicationRunner runner() { - return args -> { - endpoint.changeState("process3-in-0", State.STARTED); - }; -} -``` - -See https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#binding_visualization_control[this section] from the reference docs for more details on this mechanism. - -NOTE: When controlling the bindings by disabling `auto-startup` as described in this section, please note that this is only available for consumer bindings. -In other words, if you use the producer binding, `process3-out-0`, that does not have any effect in terms of disabling the auto starting of the processor, although this producer binding uses the same `StreamsBuilderFactoryBean` as the consumer bindings. - diff --git a/docs/modules/ROOT/pages/kafka/streams/manually-starting-processors.adoc b/docs/modules/ROOT/pages/kafka/streams/manually-starting-processors.adoc deleted file mode 100644 index 7ba74e3b4..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/manually-starting-processors.adoc +++ /dev/null @@ -1,26 +0,0 @@ -[[manually-starting-kafka-streams-processors]] -= Manually starting Kafka Streams processors -:page-section-summary-toc: 1 - -Spring Cloud Stream Kafka Streams binder offers an abstraction called `StreamsBuilderFactoryManager` on top of the `StreamsBuilderFactoryBean` from Spring for Apache Kafka. -This manager API is used for controlling the multiple `StreamsBuilderFactoryBean` per processor in a binder based application. -Therefore, when using the binder, if you manually want to control the auto starting of the various `StreamsBuilderFactoryBean` objects in the application, you need to use `StreamsBuilderFactoryManager`. -You can use the property `spring.kafka.streams.auto-startup` and set this to `false` in order to turn off auto starting of the processors. -Then, in the application, you can use something as below to start the processors using `StreamsBuilderFactoryManager`. - -``` -@Bean -public ApplicationRunner runner(StreamsBuilderFactoryManager sbfm) { - return args -> { - sbfm.start(); - }; -} -``` - -This feature is handy, when you want your application to start in the main thread and let Kafka Streams processors start separately. -For example, when you have a large state store that needs to be restored, if the processors are started normally as is the default case, this may block your application to start. -If you are using some sort of liveness probe mechanism (for example on Kubernetes), it may think that the application is down and attempt a restart. -In order to correct this, you can set `spring.kafka.streams.auto-startup` to `false` and follow the approach above. - -Keep in mind that, when using the Spring Cloud Stream binder, you are not directly dealing with `StreamsBuilderFactoryBean` from Spring for Apache Kafka, rather `StreamsBuilderFactoryManager`, as the `StreamsBuilderFactoryBean` objects are internally managed by the binder. - diff --git a/docs/modules/ROOT/pages/kafka/streams/mixing-high-level-dsl-and-low-level-processor-api.adoc b/docs/modules/ROOT/pages/kafka/streams/mixing-high-level-dsl-and-low-level-processor-api.adoc deleted file mode 100644 index 69670c5dd..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/mixing-high-level-dsl-and-low-level-processor-api.adoc +++ /dev/null @@ -1,63 +0,0 @@ -[[mixing-high-level-dsl-and-low-level-processor-api]] -= Mixing high level DSL and low level Processor API - -Kafka Streams provides two variants of APIs. -It has a higher level DSL like API where you can chain various operations that maybe familiar to a lot of functional programmers. -Kafka Streams also gives access to a low level Processor API. -The processor API, although very powerful and gives the ability to control things in a much lower level, is imperative in nature. -Kafka Streams binder for Spring Cloud Stream, allows you to use either the high level DSL or mixing both the DSL and the processor API. -Mixing both of these variants give you a lot of options to control various use cases in an application. -Applications can use the `transform` or `process` method API calls to get access to the processor API. - -Here is a look at how one may combine both the DSL and the processor API in a Spring Cloud Stream application using the `process` API. - -``` -@Bean -public Consumer> process() { - return input -> - input.process(() -> new Processor() { - @Override - @SuppressWarnings("unchecked") - public void init(ProcessorContext context) { - this.context = context; - } - - @Override - public void process(Object key, String value) { - //business logic - } - - @Override - public void close() { - - }); -} -``` - -Here is an example using the `transform` API. - -``` -@Bean -public Consumer> process() { - return (input, a) -> - input.transform(() -> new Transformer>() { - @Override - public void init(ProcessorContext context) { - - } - - @Override - public void close() { - - } - - @Override - public KeyValue transform(Object key, String value) { - // business logic - return transformed KStream; - } - }); -} -``` - -The `process` API method call is a terminal operation while the `transform` API is non terminal and gives you a potentially transformed `KStream` using which you can continue further processing using either the DSL or the processor API. - diff --git a/docs/modules/ROOT/pages/kafka/streams/multi-binders-with-based-binders-and-regular-binder.adoc b/docs/modules/ROOT/pages/kafka/streams/multi-binders-with-based-binders-and-regular-binder.adoc deleted file mode 100644 index cd6d2411f..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/multi-binders-with-based-binders-and-regular-binder.adoc +++ /dev/null @@ -1,101 +0,0 @@ -[[multi-binders-with-kafka-streams-based-binders-and-regular-kafka-binder]] -= Multi binders with Kafka Streams based binders and regular Kafka Binder - -You can have an application where you have both a function/consumer/supplier that is based on the regular Kafka binder and a Kafka Streams based processor. -However, you cannot mix both of them within a single function or consumer. - -Here is an example, where you have both binder based components within the same application. - -``` -@Bean -public Function process() { - return s -> s; -} - -@Bean -public Function, KStream> kstreamProcess() { - - return input -> input; -} - -``` - -This is the relevant parts from the configuration: - -``` -spring.cloud.function.definition=process;kstreamProcess -spring.cloud.stream.bindings.process-in-0.destination=foo -spring.cloud.stream.bindings.process-out-0.destination=bar -spring.cloud.stream.bindings.kstreamProcess-in-0.destination=bar -spring.cloud.stream.bindings.kstreamProcess-out-0.destination=foobar -``` - -Things become a bit more complex if you have the same application as above, but is dealing with two different Kafka clusters, for e.g. the regular `process` is acting upon both Kafka cluster 1 and cluster 2 (receiving data from cluster-1 and sending to cluster-2) and the Kafka Streams processor is acting upon Kafka cluster 2. -Then you have to use the https://cloud.spring.io/spring-cloud-stream/reference/html/spring-cloud-stream.html#multiple-binders[multi binder] facilities provided by Spring Cloud Stream. - -Here is how your configuration may change in that scenario. - -``` -# multi binder configuration -spring.cloud.stream.binders.kafka1.type: kafka -spring.cloud.stream.binders.kafka1.environment.spring.cloud.stream.kafka.streams.binder.brokers=${kafkaCluster-1} #Replace kafkaCluster-1 with the approprate IP of the cluster -spring.cloud.stream.binders.kafka2.type: kafka -spring.cloud.stream.binders.kafka2.environment.spring.cloud.stream.kafka.streams.binder.brokers=${kafkaCluster-2} #Replace kafkaCluster-2 with the approprate IP of the cluster -spring.cloud.stream.binders.kafka3.type: kstream -spring.cloud.stream.binders.kafka3.environment.spring.cloud.stream.kafka.streams.binder.brokers=${kafkaCluster-2} #Replace kafkaCluster-2 with the approprate IP of the cluster - -spring.cloud.function.definition=process;kstreamProcess - -# From cluster 1 to cluster 2 with regular process function -spring.cloud.stream.bindings.process-in-0.destination=foo -spring.cloud.stream.bindings.process-in-0.binder=kafka1 # source from cluster 1 -spring.cloud.stream.bindings.process-out-0.destination=bar -spring.cloud.stream.bindings.process-out-0.binder=kafka2 # send to cluster 2 - -# Kafka Streams processor on cluster 2 -spring.cloud.stream.bindings.kstreamProcess-in-0.destination=bar -spring.cloud.stream.bindings.kstreamProcess-in-0.binder=kafka3 -spring.cloud.stream.bindings.kstreamProcess-out-0.destination=foobar -spring.cloud.stream.bindings.kstreamProcess-out-0.binder=kafka3 -``` - -Pay attention to the above configuration. -We have two kinds of binders, but 3 binders all in all, first one is the regular Kafka binder based on cluster 1 (`kafka1`), then another Kafka binder based on cluster 2 (`kafka2`) and finally the `kstream` one (`kafka3`). -The first processor in the application receives data from `kafka1` and publishes to `kafka2` where both binders are based on regular Kafka binder but differnt clusters. -The second processor, which is a Kafka Streams processor consumes data from `kafka3` which is the same cluster as `kafka2`, but a different binder type. - -Since there are three different binder types available in the Kafka Streams family of binders - `kstream`, `ktable` and `globalktable` - if your application has multiple bindings based on any of these binders, that needs to be explicitly provided as the binder type. - -For e.g if you have a processor as below, - -``` -@Bean -public Function, - Function, - Function, KStream>>> enrichOrder() { - - ... -} -``` - -then, this has to be configured in a multi binder scenario as the following. -Please note that this is only needed if you have a true multi-binder scenario where there are multiple processors dealing with multiple clusters within a single application. -In that case, the binders need to be explicitly provided with the bindings to distinguish from other processor's binder types and clusters. - -``` -spring.cloud.stream.binders.kafka1.type: kstream -spring.cloud.stream.binders.kafka1.environment.spring.cloud.stream.kafka.streams.binder.brokers=${kafkaCluster-2} -spring.cloud.stream.binders.kafka2.type: ktable -spring.cloud.stream.binders.kafka2.environment.spring.cloud.stream.kafka.streams.binder.brokers=${kafkaCluster-2} -spring.cloud.stream.binders.kafka3.type: globalktable -spring.cloud.stream.binders.kafka3.environment.spring.cloud.stream.kafka.streams.binder.brokers=${kafkaCluster-2} - -spring.cloud.stream.bindings.enrichOrder-in-0.binder=kafka1 #kstream -spring.cloud.stream.bindings.enrichOrder-in-1.binder=kafka2 #ktablr -spring.cloud.stream.bindings.enrichOrder-in-2.binder=kafka3 #globalktable -spring.cloud.stream.bindings.enrichOrder-out-0.binder=kafka1 #kstream - -# rest of the configuration is omitted. - -``` - diff --git a/docs/modules/ROOT/pages/kafka/streams/overview.adoc b/docs/modules/ROOT/pages/kafka/streams/overview.adoc deleted file mode 100644 index f26e9fccc..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/overview.adoc +++ /dev/null @@ -1,17 +0,0 @@ -[[overview]] -= Overview -:page-section-summary-toc: 1 - -Spring Cloud Stream includes a binder implementation designed explicitly for https://kafka.apache.org/documentation/streams/[Apache Kafka Streams] binding. -With this native integration, a Spring Cloud Stream "processor" application can directly use the -https://kafka.apache.org/documentation/streams/developer-guide[Apache Kafka Streams] APIs in the core business logic. - -Kafka Streams binder implementation builds on the foundations provided by the https://docs.spring.io/spring-kafka/reference/html/#kafka-streams[Spring for Apache Kafka] project. - -Kafka Streams binder provides binding capabilities for the three major types in Kafka Streams - `KStream`, `KTable` and `GlobalKTable`. - -Kafka Streams applications typically follow a model in which the records are read from an inbound topic, apply business logic, and then write the transformed records to an outbound topic. -Alternatively, a Processor application with no outbound destination can be defined as well. - -In the following sections, we are going to look at the details of Spring Cloud Stream's integration with Kafka Streams. - diff --git a/docs/modules/ROOT/pages/kafka/streams/partition-support-on-the-outbound.adoc b/docs/modules/ROOT/pages/kafka/streams/partition-support-on-the-outbound.adoc deleted file mode 100644 index 8b3d50aeb..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/partition-support-on-the-outbound.adoc +++ /dev/null @@ -1,59 +0,0 @@ -[[partition-support-on-the-outbound]] -= Partition support on the outbound - -A Kafka Streams processor usually sends the processed output into an outbound Kafka topic. -If the outbound topic is partitioned and the processor needs to send the outgoing data into particular partitions, the applications needs to provide a bean of type `StreamPartitioner`. -See https://kafka.apache.org/23/javadoc/org/apache/kafka/streams/processor/StreamPartitioner.html[StreamPartitioner] for more details. -Let's see some examples. - -This is the same processor we already saw multiple times, - -``` -@Bean -public Function, KStream> process() { - - ... -} -``` - -Here is the output binding destination: - -``` -spring.cloud.stream.bindings.process-out-0.destination: outputTopic -``` - -If the topic `outputTopic` has 4 partitions, if you don't provide a partitioning strategy, Kafka Streams will use default partitioning strategy which may not be the outcome you want depending on the particular use case. -Let's say, you want to send any key that matches to `spring` to partition 0, `cloud` to partition 1, `stream` to partition 2, and everything else to partition 3. -This is what you need to do in the application. - -``` -@Bean -public StreamPartitioner streamPartitioner() { - return (t, k, v, n) -> { - if (k.equals("spring")) { - return 0; - } - else if (k.equals("cloud")) { - return 1; - } - else if (k.equals("stream")) { - return 2; - } - else { - return 3; - } - }; -} -``` - -This is a rudimentary implementation, however, you have access to the key/value of the record, the topic name and the total number of partitions. -Therefore, you can implement complex partitioning strategies if need be. - -You also need to provide this bean name along with the application configuration. - -``` -spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.streamPartitionerBeanName: streamPartitioner -``` - -Each output topic in the application needs to be configured separately like this. - diff --git a/docs/modules/ROOT/pages/kafka/streams/programming-model.adoc b/docs/modules/ROOT/pages/kafka/streams/programming-model.adoc deleted file mode 100644 index 8059c189c..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/programming-model.adoc +++ /dev/null @@ -1,410 +0,0 @@ -[[programming-model]] -= Programming Model - -When using the programming model provided by Kafka Streams binder, both the high-level https://docs.confluent.io/current/streams/developer-guide/dsl-api.html[Streams DSL] and a mix of both the higher level and the lower level https://docs.confluent.io/current/streams/developer-guide/processor-api.html[Processor-API] can be used as options. -When mixing both higher and lower level API's, this is usually achieved by invoking `transform` or `process` API methods on `KStream`. - -[[functional-style]] -== Functional Style - -Starting with Spring Cloud Stream `3.0.0`, Kafka Streams binder allows the applications to be designed and developed using the functional programming style that is available in Java 8. -This means that the applications can be concisely represented as a lambda expression of types `java.util.function.Function` or `java.util.function.Consumer`. - -Let's take a very basic example. - -[source] ----- -@SpringBootApplication -public class SimpleConsumerApplication { - - @Bean - public java.util.function.Consumer> process() { - - return input -> - input.foreach((key, value) -> { - System.out.println("Key: " + key + " Value: " + value); - }); - } -} ----- - -Albeit simple, this is a complete standalone Spring Boot application that is leveraging Kafka Streams for stream processing. -This is a consumer application with no outbound binding and only a single inbound binding. -The application consumes data and it simply logs the information from the `KStream` key and value on the standard output. -The application contains the `SpringBootApplication` annotation and a method that is marked as `Bean`. -The bean method is of type `java.util.function.Consumer` which is parameterized with `KStream`. -Then in the implementation, we are returning a Consumer object that is essentially a lambda expression. -Inside the lambda expression, the code for processing the data is provided. - -In this application, there is a single input binding that is of type `KStream`. -The binder creates this binding for the application with a name `process-in-0`, i.e. the name of the function bean name followed by a dash character (`-`) and the literal `in` followed by another dash and then the ordinal position of the parameter. -You use this binding name to set other properties such as destination. -For example, `spring.cloud.stream.bindings.process-in-0.destination=my-topic`. - -NOTE: If the destination property is not set on the binding, a topic is created with the same name as the binding (if there are sufficient privileges for the application) or that topic is expected to be already available. - -Once built as a uber-jar (e.g., `kstream-consumer-app.jar`), you can run the above example like the following. - -If the applications choose to define the functional beans using Spring's `Component` annotation, the binder also supports that model. -The above functional bean could be rewritten as below. - -``` -@Component(name = "process") -public class SimpleConsumer implements java.util.function.Consumer> { - - @Override - public void accept(KStream input) { - input.foreach((key, value) -> { - System.out.println("Key: " + key + " Value: " + value); - }); - } -} -``` - -[source] ----- -java -jar kstream-consumer-app.jar --spring.cloud.stream.bindings.process-in-0.destination=my-topic ----- - -Here is another example, where it is a full processor with both input and output bindings. -This is the classic word-count example in which the application receives data from a topic, the number of occurrences for each word is then computed in a tumbling time-window. - -[source] ----- -@SpringBootApplication -public class WordCountProcessorApplication { - - @Bean - public Function, KStream> process() { - - return input -> input - .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .map((key, value) -> new KeyValue<>(value, value)) - .groupByKey(Serialized.with(Serdes.String(), Serdes.String())) - .windowedBy(TimeWindows.of(5000)) - .count(Materialized.as("word-counts-state-store")) - .toStream() - .map((key, value) -> new KeyValue<>(key.key(), new WordCount(key.key(), value, - new Date(key.window().start()), new Date(key.window().end())))); - } - - public static void main(String[] args) { - SpringApplication.run(WordCountProcessorApplication.class, args); - } -} ----- - -Here again, this is a complete Spring Boot application. The difference here from the first application is that the bean method is of type `java.util.function.Function`. -The first parameterized type for the `Function` is for the input `KStream` and the second one is for the output. -In the method body, a lambda expression is provided that is of type `Function` and as implementation, the actual business logic is given. -Similar to the previously discussed Consumer based application, the input binding here is named as `process-in-0` by default. For the output, the binding name is automatically also set to `process-out-0`. - -Once built as an uber-jar (e.g., `wordcount-processor.jar`), you can run the above example like the following. - -[source] ----- -java -jar wordcount-processor.jar --spring.cloud.stream.bindings.process-in-0.destination=words --spring.cloud.stream.bindings.process-out-0.destination=counts ----- - -This application will consume messages from the Kafka topic `words` and the computed results are published to an output -topic `counts`. - -Spring Cloud Stream will ensure that the messages from both the incoming and outgoing topics are automatically bound as -KStream objects. As a developer, you can exclusively focus on the business aspects of the code, i.e. writing the logic -required in the processor. Setting up Kafka Streams specific configuration required by the Kafka Streams infrastructure -is automatically handled by the framework. - -The two examples we saw above have a single `KStream` input binding. In both cases, the bindings received the records from a single topic. -If you want to multiplex multiple topics into a single `KStream` binding, you can provide comma separated Kafka topics as destinations below. - -`spring.cloud.stream.bindings.process-in-0.destination=topic-1,topic-2,topic-3` - -In addition, you can also provide topic patterns as destinations if you want to match topics against a regular exression. - -`spring.cloud.stream.bindings.process-in-0.destination=input.*` - -[[multiple-input-bindings]] -=== Multiple Input Bindings - -Many non-trivial Kafka Streams applications often consume data from more than one topic through multiple bindings. -For instance, one topic is consumed as `Kstream` and another as `KTable` or `GlobalKTable`. -There are many reasons why an application might want to receive data as a table type. -Think of a use-case where the underlying topic is populated through a change data capture (CDC) mechanism from a database or perhaps the application only cares about the latest updates for downstream processing. -If the application specifies that the data needs to be bound as `KTable` or `GlobalKTable`, then Kafka Streams binder will properly bind the destination to a `KTable` or `GlobalKTable` and make them available for the application to operate upon. -We will look at a few different scenarios how multiple input bindings are handled in the Kafka Streams binder. - -[[bifunction-in-kafka-streams-binder]] -==== BiFunction in Kafka Streams Binder - -Here is an example where we have two inputs and an output. In this case, the application can leverage on `java.util.function.BiFunction`. - -[source] ----- -@Bean -public BiFunction, KTable, KStream> process() { - return (userClicksStream, userRegionsTable) -> (userClicksStream - .leftJoin(userRegionsTable, (clicks, region) -> new RegionWithClicks(region == null ? - "UNKNOWN" : region, clicks), - Joined.with(Serdes.String(), Serdes.Long(), null)) - .map((user, regionWithClicks) -> new KeyValue<>(regionWithClicks.getRegion(), - regionWithClicks.getClicks())) - .groupByKey(Grouped.with(Serdes.String(), Serdes.Long())) - .reduce(Long::sum) - .toStream()); -} ----- - -Here again, the basic theme is the same as in the previous examples, but here we have two inputs. -Java's `BiFunction` support is used to bind the inputs to the desired destinations. -The default binding names generated by the binder for the inputs are `process-in-0` and `process-in-1` respectively. The default output binding is `process-out-0`. -In this example, the first parameter of `BiFunction` is bound as a `KStream` for the first input and the second parameter is bound as a `KTable` for the second input. - -[[biconsumer-in-kafka-streams-binder]] -==== BiConsumer in Kafka Streams Binder - -If there are two inputs, but no outputs, in that case we can use `java.util.function.BiConsumer` as shown below. - -[source] ----- -@Bean -public BiConsumer, KTable> process() { - return (userClicksStream, userRegionsTable) -> {} -} ----- - -[[beyond-two-inputs]] -==== Beyond two inputs - -What if you have more than two inputs? -There are situations in which you need more than two inputs. In that case, the binder allows you to chain partial functions. -In functional programming jargon, this technique is generally known as currying. -With the functional programming support added as part of Java 8, Java now enables you to write curried functions. -Spring Cloud Stream Kafka Streams binder can make use of this feature to enable multiple input bindings. - -Let's see an example. - -[source] ----- -@Bean -public Function, - Function, - Function, KStream>>> enrichOrder() { - - return orders -> ( - customers -> ( - products -> ( - orders.join(customers, - (orderId, order) -> order.getCustomerId(), - (order, customer) -> new CustomerOrder(customer, order)) - .join(products, - (orderId, customerOrder) -> customerOrder - .productId(), - (customerOrder, product) -> { - EnrichedOrder enrichedOrder = new EnrichedOrder(); - enrichedOrder.setProduct(product); - enrichedOrder.setCustomer(customerOrder.customer); - enrichedOrder.setOrder(customerOrder.order); - return enrichedOrder; - }) - ) - ) - ); -} ----- - -Let's look at the details of the binding model presented above. -In this model, we have 3 partially applied functions on the inbound. Let's call them as `f(x)`, `f(y)` and `f(z)`. -If we expand these functions in the sense of true mathematical functions, it will look like these: `f(x) -> (fy) -> f(z) -> KStream`. -The `x` variable stands for `KStream`, the `y` variable stands for `GlobalKTable` and the `z` variable stands for `GlobalKTable`. -The first function `f(x)` has the first input binding of the application (`KStream`) and its output is the function, f(y). -The function `f(y)` has the second input binding for the application (`GlobalKTable`) and its output is yet another function, `f(z)`. -The input for the function `f(z)` is the third input for the application (`GlobalKTable`) and its output is `KStream` which is the final output binding for the application. -The input from the three partial functions which are `KStream`, `GlobalKTable`, `GlobalKTable` respectively are available for you in the method body for implementing the business logic as part of the lambda expression. - -Input bindings are named as `enrichOrder-in-0`, `enrichOrder-in-1` and `enrichOrder-in-2` respectively. Output binding is named as `enrichOrder-out-0`. - -With curried functions, you can virtually have any number of inputs. However, keep in mind that, anything more than a smaller number of inputs and partially applied functions for them as above in Java might lead to unreadable code. -Therefore if your Kafka Streams application requires more than a reasonably smaller number of input bindings, and you want to use this functional model, then you may want to rethink your design and decompose the application appropriately. - -[[output-bindings]] -=== Output Bindings - -Kafka Streams binder allows types of either `KStream` or `KTable` as output bindings. -Behind the scenes, the binder uses the `to` method on `KStream` to send the resultant records to the output topic. -If the application provides a `KTable` as output in the function, the binder still uses this technique by delegating to the `to` method of `KStream`. - -For example both functions below will work: - -``` -@Bean -public Function, KTable> foo() { - return KStream::toTable; - }; -} - -@Bean -public Function, KStream> bar() { - return KTable::toStream; -} -``` - -[[multiple-output-bindings]] -=== Multiple Output Bindings - -Kafka Streams allows writing outbound data into multiple topics. This feature is known as branching in Kafka Streams. -When using multiple output bindings, you need to provide an array of KStream (`KStream[]`) as the outbound return type. - -Here is an example: - -[source] ----- -@Bean -public Function, KStream[]> process() { - - Predicate isEnglish = (k, v) -> v.word.equals("english"); - Predicate isFrench = (k, v) -> v.word.equals("french"); - Predicate isSpanish = (k, v) -> v.word.equals("spanish"); - - return input -> { - final Map> stringKStreamMap = input - .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) - .groupBy((key, value) -> value) - .windowedBy(TimeWindows.of(Duration.ofSeconds(5))) - .count(Materialized.as("WordCounts-branch")) - .toStream() - .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, - new Date(key.window().start()), new Date(key.window().end())))) - .split() - .branch(isEnglish) - .branch(isFrench) - .branch(isSpanish) - .noDefaultBranch(); - - return stringKStreamMap.values().toArray(new KStream[0]); - }; -} ----- - -The programming model remains the same, however the outbound parameterized type is `KStream[]`. -The default output binding names are `process-out-0`, `process-out-1`, `process-out-2` respectively for the function above. -The reason why the binder generates three output bindings is because it detects the length of the returned `KStream` array as three. -Note that in this example, we provide a `noDefaultBranch()`; if we have used `defaultBranch()` instead, that would have required an extra output binding, essentially returning a `KStream` array of length four. - -[[summary-of-function-based-programming-styles-for-kafka-streams]] -=== Summary of Function based Programming Styles for Kafka Streams - -In summary, the following table shows the various options that can be used in the functional paradigm. - -|=== -|Number of Inputs |Number of Outputs |Component to use - -|1|0|java.util.function.Consumer -|2|0|java.util.function.BiConsumer -|1|1..n |java.util.function.Function -|2|1..n |java.util.function.BiFunction -|>= 3 |0..n |Use curried functions - -|=== - -* In the case of more than one output in this table, the type simply becomes `KStream[]`. - -[[function-composition-in-kafka-streams-binder]] -=== Function composition in Kafka Streams binder - -Kafka Streams binder supports minimal forms of functional composition for linear topologies. -Using the Java functional API support, you can write multiple functions and then compose them on your own using the `andThen` method. -For example, assume that you have the following two functions. - -``` -@Bean -public Function, KStream> foo() { - return input -> input.peek((s, s2) -> {}); -} - -@Bean -public Function, KStream> bar() { - return input -> input.peek((s, s2) -> {}); -} -``` - -Even without the functional composition support in the binder, you can compose these two functions as below. - -``` -@Bean -public Function, KStream> composed() { - foo().andThen(bar()); -} -``` - -Then you can provide definitions of the form `spring.cloud.function.definition=foo;bar;composed`. -With the functional composition support in the binder, you don't need to write this third function in which you are doing explicit function composition. - -You can simply do this instead: - -``` -spring.cloud.function.definition=foo|bar -``` - -You can even do this: - -``` -spring.cloud.function.definition=foo|bar;foo;bar -``` - -The composed function's default binding names in this example becomes `foobar-in-0` and `foobar-out-0`. - -[[limitations-of-functional-composition-in-kafka-streams-bincer]] -==== Limitations of functional composition in Kafka Streams bincer - -When you have `java.util.function.Function` bean, that can be composed with another function or multiple functions. -The same function bean can be composed with a `java.util.function.Consumer` as well. In this case, consumer is the last component composed. -A function can be composed with multiple functions, then end with a `java.util.function.Consumer` bean as well. - -When composing the beans of type `java.util.function.BiFunction`, the `BiFunction` must be the first function in the definition. -The composed entities must be either of type `java.util.function.Function` or `java.util.funciton.Consumer`. -In other words, you cannot take a `BiFunction` bean and then compose with another `BiFunction`. - -You cannot compose with types of `BiConsumer` or definitions where `Consumer` is the first component. -You cannot also compose with functions where the output is an array (`KStream[]` for branching) unless this is the last component in the definition. - -The very first `Function` of `BiFunction` in the function definition may use a curried form also. -For example, the following is possible. - -``` -@Bean -public Function, Function, KStream>> curriedFoo() { - return a -> b -> - a.join(b, (value1, value2) -> value1 + value2); -} - -@Bean -public Function, KStream> bar() { - return input -> input.mapValues(value -> value + "From-anotherFooFunc"); -} -``` - -and the function definition could be `curriedFoo|bar`. -Behind the scenes, the binder will create two input bindings for the curried function, and an output binding based on the final function in the definition. -The default input bindings in this case are going to be `curriedFoobar-in-0` and `curriedFoobar-in-1`. -The default output binding for this example becomes `curriedFoobar-out-0`. - -[[special-note-on-using-ktable-as-output-in-function-composition]] -==== Special note on using `KTable` as output in function composition - -Lets say you have the following two functions. - -``` -@Bean -public Function, KTable> foo() { - return KStream::toTable; - }; -} - -@Bean -public Function, KStream> bar() { - return KTable::toStream; -} -``` - -You can compose them as `foo|bar`, but keep in mind that the second function (`bar` in this case) must have a `KTable` as input since the first function (`foo`) has `KTable` as output. - diff --git a/docs/modules/ROOT/pages/kafka/streams/record-serialization-and-deserialization.adoc b/docs/modules/ROOT/pages/kafka/streams/record-serialization-and-deserialization.adoc deleted file mode 100644 index 9533b913f..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/record-serialization-and-deserialization.adoc +++ /dev/null @@ -1,140 +0,0 @@ -[[record-serialization-and-deserialization]] -= Record serialization and deserialization - -Kafka Streams binder allows you to serialize and deserialize records in two ways. -One is the native serialization and deserialization facilities provided by Kafka and the other one is the message conversion capabilities of Spring Cloud Stream framework. -Lets look at some details. - -[[inbound-deserialization]] -== Inbound deserialization - -Keys are always deserialized using native Serdes. - -For values, by default, deserialization on the inbound is natively performed by Kafka. -Please note that this is a major change on default behavior from previous versions of Kafka Streams binder where the deserialization was done by the framework. - -Kafka Streams binder will try to infer matching `Serde` types by looking at the type signature of `java.util.function.Function|Consumer`. -Here is the order that it matches the Serdes. - -* If the application provides a bean of type `Serde` and if the return type is parameterized with the actual type of the incoming key or value type, then it will use that `Serde` for inbound deserialization. -For e.g. if you have the following in the application, the binder detects that the incoming value type for the `KStream` matches with a type that is parameterized on a `Serde` bean. -It will use that for inbound deserialization. - - -``` -@Bean -public Serde customSerde() { - ... -} - -@Bean -public Function, KStream> process() { -} -``` - -* Next, it looks at the types and see if they are one of the types exposed by Kafka Streams. If so, use them. -Here are the Serde types that the binder will try to match from Kafka Streams. - - Integer, Long, Short, Double, Float, byte[], UUID and String. - -* If none of the Serdes provided by Kafka Streams don't match the types, then it will use JsonSerde provided by Spring Kafka. In this case, the binder assumes that the types are JSON friendly. -This is useful if you have multiple value objects as inputs since the binder will internally infer them to correct Java types. -Before falling back to the `JsonSerde` though, the binder checks at the default `Serde` s set in the Kafka Streams configuration to see if it is a `Serde` that it can match with the incoming KStream's types. - -If none of the above strategies worked, then the applications must provide the `Serde` s through configuration. -This can be configured in two ways - binding or default. - -First the binder will look if a `Serde` is provided at the binding level. -For e.g. if you have the following processor, - -``` -@Bean -public BiFunction, KTable, KStream> process() {...} -``` - -then, you can provide a binding level `Serde` using the following: - -``` -spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.keySerde=CustomKeySerde -spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.valueSerde=io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde - -spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.keySerde=CustomKeySerde -spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.valueSerde=io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -``` - -NOTE: If you provide `Serde` as abover per input binding, then that will takes higher precedence and the binder will stay away from any `Serde` inference. - -If you want the default key/value Serdes to be used for inbound deserialization, you can do so at the binder level. - -``` -spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde -spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde -``` - -If you don't want the native decoding provided by Kafka, you can rely on the message conversion features that Spring Cloud Stream provides. -Since native decoding is the default, in order to let Spring Cloud Stream deserialize the inbound value object, you need to explicitly disable native decoding. - -For e.g. if you have the same BiFunction processor as above, then `spring.cloud.stream.bindings.process-in-0.consumer.nativeDecoding: false` -You need to disable native decoding for all the inputs individually. Otherwise, native decoding will still be applied for those you do not disable. - -By default, Spring Cloud Stream will use `application/json` as the content type and use an appropriate json message converter. -You can use custom message converters by using the following property and an appropriate `MessageConverter` bean. -``` -spring.cloud.stream.bindings.process-in-0.contentType -``` - -[[outbound-serialization]] -== Outbound serialization - -Outbound serialization pretty much follows the same rules as above for inbound deserialization. -As with the inbound deserialization, one major change from the previous versions of Spring Cloud Stream is that the serialization on the outbound is handled by Kafka natively. -Before 3.0 versions of the binder, this was done by the framework itself. - -Keys on the outbound are always serialized by Kafka using a matching `Serde` that is inferred by the binder. -If it can't infer the type of the key, then that needs to be specified using configuration. - -Value serdes are inferred using the same rules used for inbound deserialization. -First it matches to see if the outbound type is from a provided bean in the application. -If not, it checks to see if it matches with a `Serde` exposed by Kafka such as - `Integer`, `Long`, `Short`, `Double`, `Float`, `byte[]`, `UUID` and `String`. -If that doesnt't work, then it falls back to `JsonSerde` provided by the Spring Kafka project, but first look at the default `Serde` configuration to see if there is a match. -Keep in mind that all these happen transparently to the application. -If none of these work, then the user has to provide the `Serde` to use by configuration. - -Lets say you are using the same `BiFunction` processor as above. Then you can configure outbound key/value Serdes as following. - -``` -spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.keySerde=CustomKeySerde -spring.cloud.stream.kafka.streams.bindings.process-out-0.producer.valueSerde=io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde -``` - -If Serde inference fails, and no binding level Serdes are provided, then the binder falls back to the `JsonSerde`, but look at the default Serdes for a match. - -Default serdes are configured in the same way as above where it is described under deserialization. - -`spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde` -`spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde` - -If your application uses the branching feature and has multiple output bindings, then these have to be configured per binding. -Once again, if the binder is capable of inferring the `Serde` types, you don't need to do this configuration. - -If you don't want the native encoding provided by Kafka, but want to use the framework provided message conversion, then you need to explicitly disable native encoding since since native encoding is the default. -For e.g. if you have the same BiFunction processor as above, then `spring.cloud.stream.bindings.process-out-0.producer.nativeEncoding: false` -You need to disable native encoding for all the output individually in the case of branching. Otherwise, native encoding will still be applied for those you don't disable. - -When conversion is done by Spring Cloud Stream, by default, it will use `application/json` as the content type and use an appropriate json message converter. -You can use custom message converters by using the following property and a corresponding `MessageConverter` bean. -``` -spring.cloud.stream.bindings.process-out-0.contentType -``` - -When native encoding/decoding is disabled, binder will not do any inference as in the case of native Serdes. -Applications need to explicitly provide all the configuration options. -For that reason, it is generally advised to stay with the default options for de/serialization and stick with native de/serialization provided by Kafka Streams when you write Spring Cloud Stream Kafka Streams applications. -The one scenario in which you must use message conversion capabilities provided by the framework is when your upstream producer is using a specific serialization strategy. -In that case, you want to use a matching deserialization strategy as native mechanisms may fail. -When relying on the default `Serde` mechanism, the applications must ensure that the binder has a way forward with correctly map the inbound and outbound with a proper `Serde`, as otherwise things might fail. - -It is worth to mention that the data de/serialization approaches outlined above are only applicable on the edges of your processors, i.e. - inbound and outbound. -Your business logic might still need to call Kafka Streams API's that explicitly need `Serde` objects. -Those are still the responsibility of the application and must be handled accordingly by the developer. - diff --git a/docs/modules/ROOT/pages/kafka/streams/retrying-critical-business-logic.adoc b/docs/modules/ROOT/pages/kafka/streams/retrying-critical-business-logic.adoc deleted file mode 100644 index 4083957f4..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/retrying-critical-business-logic.adoc +++ /dev/null @@ -1,105 +0,0 @@ -[[retrying-critical-business-logic]] -= Retrying critical business logic - -There are scenarios in which you might want to retry parts of your business logic that are critical to the application. -There maybe an external call to a relational database or invoking a REST endpoint from the Kafka Streams processor. -These calls can fail for various reasons such as network issues or remote service unavailability. -More often, these failures may self resolve if you can try them again. -By default, Kafka Streams binder creates `RetryTemplate` beans for all the input bindings. - -If the function has the following signature, -``` -@Bean -public java.util.function.Consumer> process() -``` -and with default binding name, the `RetryTemplate` will be registered as `process-in-0-RetryTemplate`. -This is following the convention of binding name (`process-in-0`) followed by the literal `-RetryTemplate`. -In the case of multiple input bindings, there will be a separate `RetryTemplate` bean available per binding. -If there is a custom `RetryTemplate` bean available in the application and provided through `spring.cloud.stream.bindings..consumer.retryTemplateName`, then that takes precedence over any input binding level retry template configuration properties. - -Once the `RetryTemplate` from the binding is injected into the application, it can be used to retry any critical sections of the application. -Here is an example: - -``` -@Bean -public java.util.function.Consumer> process(@Lazy @Qualifier("process-in-0-RetryTemplate") RetryTemplate retryTemplate) { - - return input -> input - .process(() -> new Processor() { - @Override - public void init(ProcessorContext processorContext) { - } - - @Override - public void process(Object o, String s) { - retryTemplate.execute(context -> { - //Critical business logic goes here. - }); - } - - @Override - public void close() { - } - }); -} -``` - -Or you can use a custom `RetryTemplate` as below. - -``` -@EnableAutoConfiguration -public static class CustomRetryTemplateApp { - - @Bean - @StreamRetryTemplate - RetryTemplate fooRetryTemplate() { - RetryTemplate retryTemplate = new RetryTemplate(); - - RetryPolicy retryPolicy = new SimpleRetryPolicy(4); - FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); - backOffPolicy.setBackOffPeriod(1); - - retryTemplate.setBackOffPolicy(backOffPolicy); - retryTemplate.setRetryPolicy(retryPolicy); - - return retryTemplate; - } - - @Bean - public java.util.function.Consumer> process() { - - return input -> input - .process(() -> new Processor() { - @Override - public void init(ProcessorContext processorContext) { - } - - @Override - public void process(Object o, String s) { - fooRetryTemplate().execute(context -> { - //Critical business logic goes here. - }); - - } - - @Override - public void close() { - } - }); - } -} -``` - -Note that when retries are exhausted, by default, the last exception will be thrown, causing the processor to terminate. -If you wish to handle the exception and continue processing, you can add a RecoveryCallback to the `execute` method: -Here is an example. -``` -retryTemplate.execute(context -> { - //Critical business logic goes here. - }, context -> { - //Recovery logic goes here. - return null; - )); -``` -Refer to the https://github.com/spring-projects/spring-retry[Spring Retry] project for more information about the RetryTemplate, retry policies, backoff policies and more. - diff --git a/docs/modules/ROOT/pages/kafka/streams/state-cleanup.adoc b/docs/modules/ROOT/pages/kafka/streams/state-cleanup.adoc deleted file mode 100644 index 881ed416b..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/state-cleanup.adoc +++ /dev/null @@ -1,9 +0,0 @@ -[[state-cleanup]] -= State Cleanup -:page-section-summary-toc: 1 - -By default, no local state is cleaned up when the binding is stopped. -This is the same behavior effective from Spring Kafka version 2.7. -See https://docs.spring.io/spring-kafka/reference/html/#streams-config[Spring Kafka documentation] for more details. -To modify this behavior simply add a single `CleanupConfig` `@Bean` (configured to clean up on start, stop, or neither) to the application context; the bean will be detected and wired into the factory bean. - diff --git a/docs/modules/ROOT/pages/kafka/streams/state-store.adoc b/docs/modules/ROOT/pages/kafka/streams/state-store.adoc deleted file mode 100644 index 0e4b96149..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/state-store.adoc +++ /dev/null @@ -1,69 +0,0 @@ -[[state-store]] -= State Store - -State stores are created automatically by Kafka Streams when the high level DSL is used and appropriate calls are made those trigger a state store. - -If you want to materialize an incoming `KTable` binding as a named state store, then you can do so by using the following strategy. - -Lets say you have the following function. - -[source] ----- -@Bean -public BiFunction, KTable, KStream> process() { - ... -} ----- - -Then by setting the following property, the incoming `KTable` data will be materialized in to the named state store. - -[source] ----- -spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.materializedAs: incoming-store ----- - -You can define custom state stores as beans in your application and those will be detected and added to the Kafka Streams builder by the binder. -Especially when the processor API is used, you need to register a state store manually. -In order to do so, you can create the StateStore as a bean in the application. -Here are examples of defining such beans. - -[source] ----- -@Bean -public StoreBuilder myStore() { - return Stores.keyValueStoreBuilder( - Stores.persistentKeyValueStore("my-store"), Serdes.Long(), - Serdes.Long()); -} - -@Bean -public StoreBuilder otherStore() { - return Stores.windowStoreBuilder( - Stores.persistentWindowStore("other-store", - 1L, 3, 3L, false), Serdes.Long(), - Serdes.Long()); -} ----- - -These state stores can be then accessed by the applications directly. - -During the bootstrap, the above beans will be processed by the binder and passed on to the Streams builder object. - -Accessing the state store: -[source] ----- -Processor() { - - WindowStore state; - - @Override - public void init(ProcessorContext processorContext) { - state = (WindowStore)processorContext.getStateStore("mystate"); - } - ... -} ----- - -This will not work when it comes to registering global state stores. -In order to register a global state store, please see the section below on customizing `StreamsBuilderFactoryBean`. - diff --git a/docs/modules/ROOT/pages/kafka/streams/streamsbuilderfactorybean-customizer.adoc b/docs/modules/ROOT/pages/kafka/streams/streamsbuilderfactorybean-customizer.adoc deleted file mode 100644 index 83b7fe869..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/streamsbuilderfactorybean-customizer.adoc +++ /dev/null @@ -1,113 +0,0 @@ -[[streamsbuilderfactorybean-customizer]] -= StreamsBuilderFactoryBean customizer - -It is often required to customize the `StreamsBuilderFactoryBean` that creates the `KafkaStreams` objects. -Based on the underlying support provided by Spring Kafka, the binder allows you to customize the `StreamsBuilderFactoryBean`. -You can use the `StreamsBuilderFactoryBeanCustomizer` to customize the `StreamsBuilderFactoryBean` itself. -Then, once you get access to the `StreamsBuilderFactoryBean` through this customizer, you can customize the corresponding `KafkaStreams` using `KafkaStreamsCustomzier`. -Both of these customizers are part of the Spring for Apache Kafka project. - -Here is an example of using the `StreamsBuilderFactoryBeanCustomizer`. - -``` -@Bean -public StreamsBuilderFactoryBeanCustomizer streamsBuilderFactoryBeanCustomizer() { - return sfb -> sfb.setStateListener((newState, oldState) -> { - //Do some action here! - }); -} -``` - -The above is shown as an illustration of the things you can do to customize the `StreamsBuilderFactoryBean`. -You can essentially call any available mutation operations from `StreamsBuilderFactoryBean` to customize it. -This customizer will be invoked by the binder right before the factory bean is started. - -Once you get access to the `StreamsBuilderFactoryBean`, you can also customize the underlying `KafkaStreams` object. -Here is a blueprint for doing so. - -``` -@Bean -public StreamsBuilderFactoryBeanCustomizer streamsBuilderFactoryBeanCustomizer() { - return factoryBean -> { - factoryBean.setKafkaStreamsCustomizer(new KafkaStreamsCustomizer() { - @Override - public void customize(KafkaStreams kafkaStreams) { - kafkaStreams.setUncaughtExceptionHandler((t, e) -> { - - }); - } - }); - }; -} -``` - -`KafkaStreamsCustomizer` will be called by the `StreamsBuilderFactoryBeabn` right before the underlying `KafkaStreams` gets started. - -There can only be one `StreamsBuilderFactoryBeanCustomizer` in the entire application. -Then how do we account for multiple Kafka Streams processors as each of them are backed up by individual `StreamsBuilderFactoryBean` objects? -In that case, if the customization needs to be different for those processors, then the application needs to apply some filter based on the application ID. - -For e.g, - -``` -@Bean -public StreamsBuilderFactoryBeanCustomizer streamsBuilderFactoryBeanCustomizer() { - - return factoryBean -> { - if (factoryBean.getStreamsConfiguration().getProperty(StreamsConfig.APPLICATION_ID_CONFIG) - .equals("processor1-application-id")) { - factoryBean.setKafkaStreamsCustomizer(new KafkaStreamsCustomizer() { - @Override - public void customize(KafkaStreams kafkaStreams) { - kafkaStreams.setUncaughtExceptionHandler((t, e) -> { - - }); - } - }); - } - }; -``` - -[[using-customizer-to-register-a-global-state-store]] -== Using Customizer to register a global state store - -As mentioned above, the binder does not provide a first class way to register global state stores as a feature. -For that, you need to use the customizer. -Here is how that can be done. - -``` -@Bean -public StreamsBuilderFactoryBeanCustomizer customizer() { - return fb -> { - try { - final StreamsBuilder streamsBuilder = fb.getObject(); - streamsBuilder.addGlobalStore(...); - } - catch (Exception e) { - - } - }; -} -``` - -Again, if you have multiple processors, you want to attach the global state store to the right `StreamsBuilder` by filtering out the other `StreamsBuilderFactoryBean` objects using the application id as outlined above. - -[[using-customizer-to-register-a-production-exception-handler]] -== Using customizer to register a production exception handler - -In the error handling section, we indicated that the binder does not provide a first class way to deal with production exceptions. -Though that is the case, you can still use the `StreamsBuilderFacotryBean` customizer to register production exception handlers. See below. - -``` -@Bean -public StreamsBuilderFactoryBeanCustomizer customizer() { - return fb -> { - fb.getStreamsConfiguration().put(StreamsConfig.DEFAULT_PRODUCTION_EXCEPTION_HANDLER_CLASS_CONFIG, - CustomProductionExceptionHandler.class); - }; -} -``` - -Once again, if you have multiple processors, you may want to set it appropriately against the correct `StreamsBuilderFactoryBean`. -You may also add such production exception handlers using the configuration property (See below for more on that), but this is an option if you choose to go with a programmatic approach. - diff --git a/docs/modules/ROOT/pages/kafka/streams/timestamp-extractor.adoc b/docs/modules/ROOT/pages/kafka/streams/timestamp-extractor.adoc deleted file mode 100644 index 8372ddcce..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/timestamp-extractor.adoc +++ /dev/null @@ -1,34 +0,0 @@ -[[timestamp-extractor]] -= Timestamp extractor - -Kafka Streams allows you to control the processing of the consumer records based on various notions of timestamp. -By default, Kafka Streams extracts the timestamp metadata embedded in the consumer record. -You can change this default behavior by providing a different `TimestampExtractor` implementation per input binding. -Here are some details on how that can be done. - -``` -@Bean -public Function, - Function, - Function, KStream>>> process() { - return orderStream -> - customers -> - products -> orderStream; -} - -@Bean -public TimestampExtractor timestampExtractor() { - return new WallclockTimestampExtractor(); -} -``` - -Then you set the above `TimestampExtractor` bean name per consumer binding. - -``` -spring.cloud.stream.kafka.streams.bindings.process-in-0.consumer.timestampExtractorBeanName=timestampExtractor -spring.cloud.stream.kafka.streams.bindings.process-in-1.consumer.timestampExtractorBeanName=timestampExtractor -spring.cloud.stream.kafka.streams.bindings.process-in-2.consumer.timestampExtractorBeanName=timestampExtractor" -``` - -If you skip an input consumer binding for setting a custom timestamp extractor, that consumer will use the default settings. - diff --git a/docs/modules/ROOT/pages/kafka/streams/topology-visualization.adoc b/docs/modules/ROOT/pages/kafka/streams/topology-visualization.adoc deleted file mode 100644 index 059c3d7ab..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/topology-visualization.adoc +++ /dev/null @@ -1,14 +0,0 @@ -[[kafka-streams-topology-visualization]] -= Kafka Streams topology visualization -:page-section-summary-toc: 1 - -Kafka Streams binder provides the following actuator endpoints for retrieving the topology description using which you can visualize the topology using external tools. - -`/actuator/kafkastreamstopology` - -`/actuator/kafkastreamstopology/` - -You need to include the actuator and web dependencies from Spring Boot to access these endpoints. -Further, you also need to add `kafkastreamstopology` to `management.endpoints.web.exposure.include` property. -By default, the `kafkastreamstopology` endpoint is disabled. - diff --git a/docs/modules/ROOT/pages/kafka/streams/tracing-using-spring-cloud-sleuth.adoc b/docs/modules/ROOT/pages/kafka/streams/tracing-using-spring-cloud-sleuth.adoc deleted file mode 100644 index 78f23c267..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/tracing-using-spring-cloud-sleuth.adoc +++ /dev/null @@ -1,64 +0,0 @@ -[[tracing-using-spring-cloud-sleuth]] -= Tracing using Spring Cloud Sleuth - -When Spring Cloud Sleuth is on the classpath of a Spring Cloud Stream Kafka Streams binder based application, both its consumer and producer are automatically instrumented with tracing information. -However, in order to trace any application specific operations, those need to be explicitly instrumented by the user code. -This can be done by injecting the `KafkaStreamsTracing` bean from Spring Cloud Sleuth in the application and then invoke various Kafka Streams operations through this injected bean. -Here are some examples of using it. - -``` -@Bean -public BiFunction, KTable, KStream> clicks(KafkaStreamsTracing kafkaStreamsTracing) { - return (userClicksStream, userRegionsTable) -> (userClicksStream - .transformValues(kafkaStreamsTracing.peek("span-1", (key, value) -> LOG.info("key/value: " + key + "/" + value))) - .leftJoin(userRegionsTable, (clicks, region) -> new RegionWithClicks(region == null ? - "UNKNOWN" : region, clicks), - Joined.with(Serdes.String(), Serdes.Long(), null)) - .transform(kafkaStreamsTracing.map("span-2", (key, value) -> { - LOG.info("Click Info: " + value.getRegion() + "/" + value.getClicks()); - return new KeyValue<>(value.getRegion(), - value.getClicks()); - })) - .groupByKey(Grouped.with(Serdes.String(), Serdes.Long())) - .reduce(Long::sum, Materialized.as(CLICK_UPDATES)) - .toStream()); -} -``` - -In the example above, there are two places where it adds explicit tracing instrumentation. -First, we are logging the key/value information from the incoming `KStream`. -When this information is logged, the associated span and trace IDs get logged as well so that a monitoring system can track them and correlate with the same span id. -Second, when we call a `map` operation, instead of calling it directly on the `KStream` class, we wrap it inside a `transform` operation and then call `map` from `KafkaStreamsTracing`. -In this case also, the logged message will contain the span ID and trace ID. - -Here is another example, where we use the low-level transformer API for accessing the various Kafka Streams headers. -When spring-cloud-sleuth is on the classpath, all the tracing headers can also be accessed like this. - -``` -@Bean -public Function, KStream> process(KafkaStreamsTracing kafkaStreamsTracing) { - return input -> input.transform(kafkaStreamsTracing.transformer( - "transformer-1", - () -> new Transformer>() { - ProcessorContext context; - - @Override - public void init(ProcessorContext context) { - this.context = context; - } - - @Override - public KeyValue transform(String key, String value) { - LOG.info("Headers: " + this.context.headers()); - LOG.info("K/V:" + key + "/" + value); - // More transformations, business logic execution, etc. go here. - return KeyValue.pair(key, value); - } - - @Override - public void close() { - } - })); -} -``` - diff --git a/docs/modules/ROOT/pages/kafka/streams/usage.adoc b/docs/modules/ROOT/pages/kafka/streams/usage.adoc deleted file mode 100644 index 7c347e582..000000000 --- a/docs/modules/ROOT/pages/kafka/streams/usage.adoc +++ /dev/null @@ -1,18 +0,0 @@ -[[usage]] -= Usage -:page-section-summary-toc: 1 - -For using the Kafka Streams binder, you just need to add it to your Spring Cloud Stream application, using the following maven coordinates: - -[source,xml] ----- - - org.springframework.cloud - spring-cloud-stream-binder-kafka-streams - ----- - -A quick way to bootstrap a new project for Kafka Streams binder is to use http://start.spring.io[Spring Initializr] and then select "Cloud Streams" and "Spring for Kafka Streams" as shown below - -image::{github-raw}/docs/src/main/asciidoc/images/spring-initializr-kafka-streams.png[width=800,scaledwidth="75%",align="center"] - diff --git a/docs/modules/ROOT/pages/preface.adoc b/docs/modules/ROOT/pages/preface.adoc deleted file mode 100644 index 6926b27f0..000000000 --- a/docs/modules/ROOT/pages/preface.adoc +++ /dev/null @@ -1,174 +0,0 @@ -[[a-brief-history-of-spring-s-data-integration-journey]] -= A Brief History of Spring's Data Integration Journey - -Spring's journey on Data Integration started with https://projects.spring.io/spring-integration/[Spring Integration]. With its programming model, it provided a consistent developer experience to build applications that can embrace http://www.enterpriseintegrationpatterns.com/[Enterprise Integration Patterns] to connect with external systems such as, databases, message brokers, and among others. - -Fast forward to the cloud-era, where microservices have become prominent in the enterprise setting. https://projects.spring.io/spring-boot/[Spring Boot] transformed the way how developers built Applications. With Spring's programming model and the runtime responsibilities handled by Spring Boot, it became seamless to develop stand-alone, production-grade Spring-based microservices. - -To extend this to Data Integration workloads, Spring Integration and Spring Boot were put together into a new project. Spring Cloud Stream was born. - -[%hardbreaks] -With Spring Cloud Stream, developers can: - -- Build, test and deploy data-centric applications in isolation. -- Apply modern microservices architecture patterns, including composition through messaging. -- Decouple application responsibilities with event-centric thinking. An event can represent something that has happened in time, to which the downstream consumer applications can react without knowing where it originated or the producer's identity. -- Port the business logic onto message brokers (such as RabbitMQ, Apache Kafka, Amazon Kinesis). -- Rely on the framework's automatic content-type support for common use-cases. Extending to different data conversion types is possible. -- and many more. . . - -[[quick-start]] -= Quick Start - -You can try Spring Cloud Stream in less than 5 min even before you jump into any details by following this three-step guide. - -We show you how to create a Spring Cloud Stream application that receives messages coming from the messaging middleware of your choice (more on this later) and logs received messages to the console. -We call it `LoggingConsumer`. -While not very practical, it provides a good introduction to some of the main concepts -and abstractions, making it easier to digest the rest of this user guide. - -The three steps are as follows: - -. xref:preface.adoc#spring-cloud-stream-preface-creating-sample-application[Creating a Sample Application by Using Spring Initializr] -. xref:preface.adoc#spring-cloud-stream-preface-importing-project[Importing the Project into Your IDE] -. xref:preface.adoc#spring-cloud-stream-preface-adding-message-handler[Adding a Message Handler, Building, and Running] - -[[spring-cloud-stream-preface-creating-sample-application]] -== Creating a Sample Application by Using Spring Initializr -To get started, visit the https://start.spring.io[Spring Initializr]. From there, you can generate our `LoggingConsumer` application. To do so: - -. In the *Dependencies* section, start typing `stream`. -When the "`Cloud Stream`" option should appears, select it. -. Start typing either 'kafka' or 'rabbit'. -. Select "`Kafka`" or "`RabbitMQ`". -+ -Basically, you choose the messaging middleware to which your application binds. -We recommend using the one you have already installed or feel more comfortable with installing and running. -Also, as you can see from the Initilaizer screen, there are a few other options you can choose. -For example, you can choose Gradle as your build tool instead of Maven (the default). -. In the *Artifact* field, type 'logging-consumer'. -+ -The value of the *Artifact* field becomes the application name. -If you chose RabbitMQ for the middleware, your Spring Initializr should now be as follows: - -[%hardbreaks] -[%hardbreaks] -[%hardbreaks] -image::spring-initializr.png[align="center"] - -[%hardbreaks] -[%hardbreaks] - -. Click the *Generate Project* button. -+ -Doing so downloads the zipped version of the generated project to your hard drive. -. Unzip the file into the folder you want to use as your project directory. - -TIP: We encourage you to explore the many possibilities available in the Spring Initializr. -It lets you create many different kinds of Spring applications. - -[[spring-cloud-stream-preface-importing-project]] -== Importing the Project into Your IDE - -Now you can import the project into your IDE. -Keep in mind that, depending on the IDE, you may need to follow a specific import procedure. -For example, depending on how the project was generated (Maven or Gradle), you may need to follow specific import procedure (for example, in Eclipse or STS, you need to use File -> Import -> Maven -> Existing Maven Project). - -Once imported, the project must have no errors of any kind. Also, `src/main/java` should contain `com.example.loggingconsumer.LoggingConsumerApplication`. - -Technically, at this point, you can run the application's main class. -It is already a valid Spring Boot application. -However, it does not do anything, so we want to add some code. - -[[spring-cloud-stream-preface-adding-message-handler]] -== Adding a Message Handler, Building, and Running - -Modify the `com.example.loggingconsumer.LoggingConsumerApplication` class to look as follows: - -[source, java] ----- -@SpringBootApplication -public class LoggingConsumerApplication { - - public static void main(String[] args) { - SpringApplication.run(LoggingConsumerApplication.class, args); - } - - @Bean - public Consumer log() { - return person -> { - System.out.println("Received: " + person); - }; - } - - public static class Person { - private String name; - public String getName() { - return name; - } - public void setName(String name) { - this.name = name; - } - public String toString() { - return this.name; - } - } -} ----- - -As you can see from the preceding listing: - -* We are using functional programming model (see <>) to define a single message handler as `Consumer`. -* We are relying on framework conventions to bind such handler to the input destination binding exposed by the binder. - -Doing so also lets you see one of the core features of the framework: It tries to automatically convert incoming message payloads to type `Person`. - -You now have a fully functional Spring Cloud Stream application that does listens for messages. -From here, for simplicity, we assume you selected RabbitMQ in xref:preface.adoc#spring-cloud-stream-preface-creating-sample-application[step one]. -Assuming you have RabbitMQ installed and running, you can start the application by running its `main` method in your IDE. - -You should see following output: - -[source] ----- - --- [ main] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg, bound to: input - --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [localhost:5672] - --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#2a3a299:0/SimpleConnection@66c83fc8. . . - . . . - --- [ main] o.s.i.a.i.AmqpInboundChannelAdapter : started inbound.input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg - . . . - --- [ main] c.e.l.LoggingConsumerApplication : Started LoggingConsumerApplication in 2.531 seconds (JVM running for 2.897) ----- - -Go to the RabbitMQ management console or any other RabbitMQ client and send a message to `input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg`. -The `anonymous.CbMIwdkJSBO1ZoPDOtHtCg` part represents the group name and is generated, so it is bound to be different in your environment. -For something more predictable, you can use an explicit group name by setting `spring.cloud.stream.bindings.input.group=hello` (or whatever name you like). - -The contents of the message should be a JSON representation of the `Person` class, as follows: - - {"name":"Sam Spade"} - -Then, in your console, you should see: - -`Received: Sam Spade` - -You can also build and package your application into a boot jar (by using `./mvnw clean install`) and run the built JAR by using the `java -jar` command. - -Now you have a working (albeit very basic) Spring Cloud Stream application. - - -[[spel-and-streaming-data]] - -[[spring-expression-language-spel-in-the-context-of-streaming-data]] -== Spring Expression Language (SpEL) in the context of Streaming data - -Throughout this reference manual you will encounter many features and examples where you can utilize Spring Expression Language (SpEL). It is important to understand certain limitations when it comes to using it. - -SpEL gives you access to the current Message as well as the Application Context you are running in. -However it is important to understand what type of data SpEL can see especially in the context of the incoming Message. -From the broker, the message arrives in a form of a byte[]. It is then transformed to a `Message` by the binders where as you can see the payload of the message maintains its raw form. The headers of the message are ``, where values are typically another primitive or a collection/array of primitives, hence Object. -That is because binder does not know the required input type as it has no access to the user code (function). So effectively binder delivered an envelope with the payload and some readable meta-data in the form of message headers, just like the letter delivered by mail. -This means that while accessing payload of the message is possible you will only have access to it as raw data (i.e., byte[]). And while it may be very common for developers to ask for ability to have SpEL access to fields of a payload object as concrete type (e.g., Foo, Bar etc), you can see how difficult or even impossible would it be to achieve. -Here is one example to demonstrate the problem; Imagine you have a routing expression to route to different functions based on payload type. This requirement would imply payload conversion from byte[] to a specific type and then applying the SpEL. However, in order to perform such conversion we would need to know the actual type to pass to converter and that comes from function's signature which we don’t know which one. A better approach to solve this requirement would be to pass the type information as message headers (e.g., `application/json;type=foo.bar.Baz` ). You’ll get a clear readable String value that could be accessed and evaluated in a year and easy to read SpEL expression. - -Additionally it is considered very bad practice to use payload for routing decisions, since the payload is considered to be privileged data - data only to be read by its final recipient. Again, using the mail delivery analogy you would not want the mailman to open your envelope and read the contents of the letter to make some delivery decisions. The same concept applies here, especially when it is relatively easy to include such information when generating a Message. It enforces certain level of discipline related to the design of data to be transmitted over the network and which pieces of such data can be considered as public and which are privileged. diff --git a/docs/modules/ROOT/pages/pulsar/pulsar_binder.adoc b/docs/modules/ROOT/pages/pulsar/pulsar_binder.adoc deleted file mode 100644 index dc5e11b9a..000000000 --- a/docs/modules/ROOT/pages/pulsar/pulsar_binder.adoc +++ /dev/null @@ -1,267 +0,0 @@ -[[pulsar-binder]] -= Spring Cloud Stream Binder for Apache Pulsar - -Spring for Apache Pulsar provides a binder for Spring Cloud Stream that we can use to build event-driven microservices using pub-sub paradigms. -In this section, we will go through the basic details of this binder. - -[[usage]] -== Usage - -We need to include the following dependency on your application to use Apache Pulsar binder for Spring Cloud Stream. - -[source,xml,indent=0,subs="verbatim,attributes",role="primary"] -.Maven ----- - - - org.springframework.pulsar - spring-pulsar-spring-cloud-stream-binder - {spring-pulsar-version} - - ----- - -[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] -.Gradle ----- -dependencies { - implementation 'org.springframework.pulsar:spring-pulsar-spring-cloud-stream-binder:{spring-pulsar-version}' -} ----- - -[[overview]] -== Overview - -The Spring Cloud Stream binder for Apache Pulsar allows the applications to focus on business logic rather than dealing with the lower-level details of managing and maintaining Pulsar. -The binder takes care of all those details for the application developer. -Spring Cloud Stream brings a powerful programming model based on {spring-cloud-function}[Spring Cloud Function] that allows the app developer to write complex event-driven applications using a functional style. -Applications can start from a middleware-neutral manner and then map Pulsar topics as destinations in Spring Cloud Stream through Spring Boot configuration properties. -Spring Cloud Stream is built on top of Spring Boot, and when writing an event-driven microservice using Spring Cloud Stream, you are essentially writing a Boot application. -Here is a straightforward Spring Cloud Stream application. - -==== -[source, java] ----- -@SpringBootApplication -public class SpringPulsarBinderSampleApp { - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - public static void main(String[] args) { - SpringApplication.run(SpringPulsarBinderSampleApp.class, args); - } - - @Bean - public Supplier