Initial commit

Introducing spring-pulsar.

This commit introduces the Spring Pulsar project
that gives Spring friendly API's for Apache Pulsar.

Currently, it has the following components.

1. PulsarTemplate for publishing to Pulsar
2. Pulsar producer factory
3. PulsarListener annotation
4. Pulsar consumer factory
5. Pulsar message listener infrastructure that encapsulates the consumer
6. Pulsar client factory bean
7. Basic Spring Boot auto configuration
8. Testcontainer based Pulsar integration tests
This commit is contained in:
Soby Chacko
2022-06-22 19:03:04 -04:00
commit b1e44a9e1a
76 changed files with 8271 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.iml
*.ipr
*.iws
.classpath
.gradle
.idea
.project
.settings
.sts4-cache
.checkstyle
bin
build
out
target
.DS_Store

66
README.adoc Normal file
View File

@@ -0,0 +1,66 @@
# Spring for Apache Pulsar
This project provides a basic Spring friendly API for developing https://pulsar.apache.org/[Apache Pulsar] applications.
Most of the ideas in this project are borrowed from the Spring for Apache Kafka project, thus a familiarity with the Spring support available in Spring for Apache Kafka would help a lot.
** #IMPORTANT#: This is a WIP project. Many of the support available at the moment are prototypes and experimental.
## Quick Introduction
The current prototype supports a basic `PulsarTemplate` for publishing to a Pulsar topic, `PulsarListner` annotation for consuming from a topic.
It also provides Spring Boot auto-configuration for these components.
### Writing a quick Spring Boot based Pulsar application.
Let's take a look at the application below.
This captures the current support available for publishing to and consuming from a Pulsar topic.
```
@SpringBootApplication
public class PulsarBootApp {
public static void main(String[] args) {
SpringApplication.run(PulsarBootApp.class, args);
}
@Bean
public ApplicationRunner runner(PulsarTemplate<String> pulsarTemplate) {
pulsarTemplate.setDefaultTopicName("hello-pulsar-exclusive");
return args -> {
for (int i = 0; i < 100; i ++) {
pulsarTemplate.send("This is message " + (i + 1));
}
};
}
@PulsarListener(subscriptionName = "test-exclusive-sub", topics = "hello-pulsar-exclusive")
public void listen(String foo) {
System.out.println("Message Received: " + foo);
}
}
```
This is a complete Spring Boot application that is sending messages to a topic in Apache Pulsar and consuming from that topic.
`PulsarTemplate` is the template mechanism available in Spring for Apache Pulsar that enables you to publish messages to a topic.
The project provides Spring Boot auto-configuration for this `PulsarTemplate` which we inject in the application above.
We use `PulsarListener` annotation to consume from the same topic where the template is publishing to.
At the moment, we only support the `Exclusive` subscription mode.
### Building the project
```
./gradlew clean build
```
### More support to come -- stay tuned...

271
build.gradle Normal file
View File

@@ -0,0 +1,271 @@
buildscript {
// repositories {
// mavenCentral()
// maven { url 'https://plugins.gradle.org/m2' }
// maven { url 'https://repo.spring.io/plugins-release' }
// mavenLocal()
// }
repositories {
mavenCentral()
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release' }
}
}
plugins {
id 'base'
id 'project-report'
id 'idea'
id 'org.sonarqube' version '2.8'
// id 'org.ajoberstar.grgit' version '4.0.1' apply false
id 'io.spring.nohttp' version '0.0.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE' apply false
id 'com.jfrog.artifactory' version '4.18.2' apply false
id 'org.asciidoctor.jvm.pdf' version '3.3.2'
id 'org.asciidoctor.jvm.gems' version '3.3.2'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
apply plugin: 'io.spring.nohttp'
//def gitPresent = new File('.git').exists()
//if(gitPresent) {
// apply plugin: 'org.ajoberstar.grgit'
//}
description = 'Spring for Apache Pulsar'
ext {
// if (gitPresent) {
// modifiedFiles =
// files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') }
// }
assertjVersion = '3.21.0'
awaitilityVersion = '4.1.1'
googleJsr305Version = '3.0.2'
hamcrestVersion = '2.2'
hibernateValidationVersion = '6.2.3.Final'
jacksonBomVersion = '2.13.2.20220328'
jaywayJsonPathVersion = '2.6.0'
junit4Version = '4.13.2'
junitJupiterVersion = '5.8.2'
pulsarVersion = '2.10.0'
log4jVersion = '2.17.2'
// micrometerVersion = '2.0.0-SNAPSHOT'
mockitoVersion = '4.0.0'
reactorVersion = '2020.0.17'
springBootVersion = '3.0.0-SNAPSHOT' // docs module
springRetryVersion = '1.3.2'
springVersion = '6.0.0-SNAPSHOT'
idPrefix = 'pulsar'
}
nohttp {
source.include '**/src/**'
source.exclude '**/*.gif', '**/*.ks'
}
allprojects {
group = 'org.springframework.pulsar'
apply plugin: 'io.spring.dependency-management'
dependencyManagement {
resolutionStrategy {
cacheChangingModulesFor 0, 'seconds'
}
applyMavenExclusions = false
generatedPomCustomization {
enabled = false
}
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:$jacksonBomVersion"
mavenBom "org.junit:junit-bom:$junitJupiterVersion"
mavenBom "org.springframework:spring-framework-bom:$springVersion"
mavenBom "io.projectreactor:reactor-bom:$reactorVersion"
}
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/release' }
maven { url 'https://repo.spring.io/milestone' }
if (version.endsWith('SNAPSHOT')) {
maven { url 'https://repo.spring.io/snapshot' }
}
// maven { url 'https://repository.apache.org/content/groups/staging/' }
}
}
subprojects { subproject ->
apply plugin: 'java-library'
apply plugin: 'java'
// apply from: "${rootProject.projectDir}/publish-maven.gradle"
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'jacoco'
// apply plugin: 'checkstyle'
java {
withJavadocJar()
withSourcesJar()
registerFeature('optional') {
usingSourceSet(sourceSets.main)
}
registerFeature('provided') {
usingSourceSet(sourceSets.main)
}
}
compileJava {
sourceCompatibility = 17
targetCompatibility = 17
}
compileTestJava {
sourceCompatibility = 17
options.encoding = 'UTF-8'
}
eclipse.project.natures += 'org.springframework.ide.eclipse.core.springnature'
jacoco {
toolVersion = '0.8.6'
}
configurations {
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
// dependencies that are common across all java projects
dependencies {
implementation "com.google.code.findbugs:jsr305:$googleJsr305Version"
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// To avoid compiler warnings about @API annotations in JUnit code
testCompileOnly 'org.apiguardian:apiguardian-api:1.0.0'
testRuntimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion"
testImplementation("org.awaitility:awaitility:$awaitilityVersion") {
exclude group: 'org.hamcrest'
}
testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion"
optionalApi "org.assertj:assertj-core:$assertjVersion"
testImplementation("org.testcontainers:pulsar:1.17.2") {
exclude module: 'log4j-to-slf4j'
}
}
// enable all compiler warnings; individual projects may customize further
[compileJava, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options']
test {
testLogging {
events "skipped", "failed"
showStandardStreams = project.hasProperty("showStandardStreams") ?: false
showExceptions = true
showStackTraces = true
exceptionFormat = 'full'
}
maxHeapSize = '1536m'
// jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8111'
jacoco {
destinationFile = file("$buildDir/jacoco.exec")
}
useJUnitPlatform()
if (System.properties['sonar.host.url']) {
finalizedBy jacocoTestReport
}
}
checkstyle {
configDirectory.set(rootProject.file("src/checkstyle"))
toolVersion = '9.0'
}
jacocoTestReport {
reports {
xml.enabled true
csv.enabled false
html.enabled false
xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
}
}
jar {
manifest {
attributes(
'Implementation-Version': archiveVersion,
// 'Created-By': "JDK ${System.properties['java.version']} (${System.properties['java.specification.vendor']})",
'Implementation-Title': subproject.name,
'Implementation-Vendor-Id': subproject.group,
'Implementation-Vendor': 'Pivotal Software, Inc.',
// 'Implementation-URL': linkHomepage,
'Automatic-Module-Name': subproject.name.replace('-', '.') // for Jigsaw
)
}
from("${rootProject.projectDir}/src/dist") {
include 'license.txt'
include 'notice.txt'
into 'META-INF'
expand(copyright: new Date().format('yyyy'), version: project.version)
}
}
}
project ('spring-pulsar') {
description = 'Spring Pulsar Support'
dependencies {
api "org.springframework.boot:spring-boot:$springBootVersion"
api "org.springframework.boot:spring-boot-autoconfigure:$springBootVersion"
api "org.springframework.boot:spring-boot-starter:$springBootVersion"
api "org.springframework.boot:spring-boot-starter-logging:$springBootVersion"
api "org.springframework.boot:spring-boot-starter-validation:$springBootVersion"
api 'org.springframework:spring-context'
api 'org.springframework:spring-messaging'
api 'org.springframework:spring-tx'
api ("org.springframework.retry:spring-retry:$springRetryVersion") {
exclude group: 'org.springframework'
}
api "org.apache.pulsar:pulsar-client:$pulsarVersion"
api "org.apache.pulsar:pulsar-client-admin:$pulsarVersion"
api "org.apache.pulsar:pulsar-client-admin-api:$pulsarVersion"
optionalApi 'com.fasterxml.jackson.core:jackson-core'
optionalApi 'com.fasterxml.jackson.core:jackson-databind'
optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8'
optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda'
optionalApi ('com.fasterxml.jackson.module:jackson-module-kotlin') {
exclude group: 'org.jetbrains.kotlin'
}
optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion"
optionalApi 'io.projectreactor:reactor-core'
// optionalApi "io.micrometer:micrometer-core:$micrometerVersion"
testImplementation 'io.projectreactor:reactor-test'
testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion"
testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion"
}
}

3
gradle.properties Normal file
View File

@@ -0,0 +1,3 @@
version=0.1.0-SNAPSHOT
org.gradle.caching=true
org.gradle.parallel=true

View File

@@ -0,0 +1,90 @@
tagOs()
tagIde()
tagCiOrLocal()
addCiMetadata()
addGitMetadata()
//addTestTaskMetadata()
void tagOs() {
gradleEnterprise.buildScan.tag System.getProperty('os.name')
}
void tagIde() {
if (System.getProperty('idea.version')) {
gradleEnterprise.buildScan.tag 'IntelliJ IDEA'
} else if (System.getProperty('eclipse.buildId')) {
gradleEnterprise.buildScan.tag 'Eclipse'
}
}
void tagCiOrLocal() {
gradleEnterprise.buildScan.tag(isCi() ? 'CI' : 'LOCAL')
}
void addGitMetadata() {
gradleEnterprise.buildScan.background {
def gitCommitId = execAndGetStdout('git', 'rev-parse', '--short=8', '--verify', 'HEAD')
def gitBranchName = execAndGetStdout('git', 'rev-parse', '--abbrev-ref', 'HEAD')
def gitStatus = execAndGetStdout('git', 'status', '--porcelain')
if(gitCommitId) {
def commitIdLabel = 'Git Commit ID'
value commitIdLabel, gitCommitId
link 'Git commit build scans', customValueSearchUrl([(commitIdLabel): gitCommitId])
}
if (gitBranchName) {
tag gitBranchName
value 'Git branch', gitBranchName
}
if (gitStatus) {
tag 'dirty'
value 'Git status', gitStatus
}
}
}
void addCiMetadata() {
def ciBuild = 'CI BUILD'
if (isBamboo()) {
gradleEnterprise.buildScan.link ciBuild, System.getenv('bamboo_resultsUrl')
}
}
void addTestTaskMetadata() {
allprojects {
tasks.withType(Test) { test ->
doFirst {
gradleEnterprise.buildScan.value "Test#maxParallelForks[${test.path}]", test.maxParallelForks.toString()
}
}
}
}
boolean isCi() {
isBamboo()
}
boolean isBamboo() {
System.getenv('bamboo_resultsUrl')
}
String execAndGetStdout(String... args) {
def stdout = new ByteArrayOutputStream()
exec {
commandLine(args)
standardOutput = stdout
}
return stdout.toString().trim()
}
String customValueSearchUrl(Map<String, String> search) {
def query = search.collect { name, value ->
"search.names=${encodeURL(name)}&search.values=${encodeURL(value)}"
}.join('&')
"$gradleEnterprise.buildScan.server/scans?$query"
}
String encodeURL(String url) {
URLEncoder.encode(url, 'UTF-8')
}

16
gradle/docs.gradle Normal file
View File

@@ -0,0 +1,16 @@
configurations {
asciidoctorExt
}
dependencies {
asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.5.0")
}
repositories {
maven {
url "https://repo.spring.io/release"
mavenContent {
includeGroup "io.spring.asciidoctor"
}
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548

234
gradlew vendored Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
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
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem 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, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

21
settings.gradle Normal file
View File

@@ -0,0 +1,21 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url 'https://repo.spring.io/plugins-release' }
}
}
plugins {
id 'com.gradle.enterprise' version '3.5'
id "io.spring.ge.conventions" version "0.0.7"
}
gradleEnterprise {
buildScan {
publishOnFailure()
}
}
rootProject.name = 'spring-pulsar-dist'
include 'spring-pulsar'

View File

@@ -0,0 +1,66 @@
package experiments.basic;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PulsarAppTry {
public static void main(String[] args) {
SpringApplication.run(PulsarAppTry.class, args);
}
// @Bean
// public PulsarProducerFactory<String> pulsarProducerFactory(PulsarClient pulsarClient) {
// Map<String, Object> config = new HashMap<>();
// config.put("topicName", "foo-1");
// return new DefaultPulsarProducerFactory<>(pulsarClient, config);
// }
//
// @Bean
// public PulsarClientFactoryBean pulsarClientFactoryBean(PulsarClientConfiguration pulsarClientConfiguration) {
// return new PulsarClientFactoryBean(pulsarClientConfiguration);
// }
//
// @Bean
// public PulsarClientConfiguration pulsarClientConfiguration() {
// return new PulsarClientConfiguration();
// }
// @Bean
// public PulsarTemplate<String> pulsarTemplate(PulsarProducerFactory<String> pulsarProducerFactory) {
// return new PulsarTemplate<>(pulsarProducerFactory);
// }
//
// @Bean
// public PulsarConsumerFactory<?> pulsarConsumerFactory(PulsarClient pulsarClient) {
//
// Map<String, Object> config = new HashMap<>();
//// final HashSet<String> strings = new HashSet<>();
//// strings.add("foobar-012");
//// config.put("topicNames", strings);
//// config.put("subscriptionName", "foobar-sb-012");
//
// return new DefaultPulsarConsumerFactory<>(pulsarClient, config);
// }
//
// @Bean
// PulsarListenerContainerFactory<?> pulsarListenerContainerFactory(PulsarConsumerFactory<Object> pulsarConsumerFactory) {
// final PulsarListenerContainerFactoryImpl<?, ?> pulsarListenerContainerFactory = new PulsarListenerContainerFactoryImpl<>();
// pulsarListenerContainerFactory.setPulsarConsumerFactory(pulsarConsumerFactory);
// return pulsarListenerContainerFactory;
// }
// @PulsarListener(subscriptionName = "hello-pulsar-listener", topics = "foo-1")
// public void listen(String foo) {
// System.out.println("Message Received: " + foo);
// }
// @Configuration(proxyBeanMethods = false)
// @EnablePulsar
// static class EnablePulsarConfiguration {
//
// }
}

View File

@@ -0,0 +1,36 @@
package experiments.basic;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.pulsar.annotation.PulsarListener;
import org.springframework.pulsar.core.PulsarTemplate;
@SpringBootApplication
public class PulsarBootApp {
public static void main(String[] args) {
SpringApplication.run(PulsarBootApp.class, args);
}
@Bean
public ApplicationRunner runner(PulsarTemplate<String> pulsarTemplate) {
pulsarTemplate.setDefaultTopicName("hello-pulsar-exclusive");
return args -> {
// for (int i = 0; i < 100; i ++) {
// pulsarTemplate.send("This is message " + (i + 1));
// }
pulsarTemplate.send("This is message ");
};
}
@PulsarListener(subscriptionName = "test-exclusive-sub", topics = "hello-pulsar-exclusive")
public void listen(String foo) {
System.out.println("Message Received: " + foo);
}
}

View File

@@ -0,0 +1,78 @@
package experiments.failover.consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageRouter;
import org.apache.pulsar.client.api.TopicMetadata;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.pulsar.annotation.PulsarListener;
import org.springframework.pulsar.core.PulsarTemplate;
@SpringBootApplication
public class FailoverConsumerApp {
public static void main(String[] args) {
String[] args1 = new String[]{"--spring.pulsar.consumer.subscription-type=Failover",
"--spring.pulsar.producer.messageRoutingMode=CustomPartition"};
SpringApplication.run(FailoverConsumerApp.class, args1);
}
@Bean
public ApplicationRunner runner(PulsarTemplate<String> pulsarTemplate) {
pulsarTemplate.setDefaultTopicName("failover-demo-topic");
return args -> {
for (int i = 0; i < 10; i++) {
pulsarTemplate.sendAsync("hello john doe 0 ", new FooRouter());
pulsarTemplate.sendAsync("hello alice doe 1", new BarRouter());
pulsarTemplate.sendAsync("hello buzz doe 2", new BuzzRouter());
Thread.sleep(1_000);
System.out.println("------------------------");
}
System.exit(0);
};
}
@PulsarListener(subscriptionName = "failover-subscription-demo", topics = "failover-demo-topic")
public void listen1(String foo) {
System.out.println("Message Received 1: " + foo);
}
@PulsarListener(subscriptionName = "failover-subscription-demo", topics = "failover-demo-topic")
public void listen2(String foo) {
System.out.println("Message Received 2: " + foo);
}
@PulsarListener(subscriptionName = "failover-subscription-demo", topics = "failover-demo-topic")
public void listen(String foo) {
System.out.println("Message Received 3: " + foo);
}
static class FooRouter implements MessageRouter {
@Override
public int choosePartition(Message<?> msg, TopicMetadata metadata) {
return 0;
}
}
static class BarRouter implements MessageRouter {
@Override
public int choosePartition(Message<?> msg, TopicMetadata metadata) {
return 1;
}
}
static class BuzzRouter implements MessageRouter {
@Override
public int choosePartition(Message<?> msg, TopicMetadata metadata) {
return 2;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar;
import org.springframework.core.NestedRuntimeException;
/**
* @author Soby Chacko
*/
public class PulsarException extends NestedRuntimeException {
public PulsarException(String msg) {
super(msg);
}
public PulsarException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.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.context.annotation.Import;
/**
* Enable Pulsar listener annotated endpoints that are created under the covers by a
* {@link org.springframework.pulsar.config.AbstractPulsarListenerContainerFactory}.
*
* @author Soby Chacko
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(PulsarListenerConfigurationSelector.class)
public @interface EnablePulsar {
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.annotation;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.pulsar.annotation.PulsarListenerAnnotationBeanPostProcessor;
import org.springframework.pulsar.config.PulsarListenerConfigUtils;
import org.springframework.pulsar.config.PulsarListenerEndpointRegistry;
/**
* An {@link ImportBeanDefinitionRegistrar} class that registers a {@link PulsarListenerAnnotationBeanPostProcessor}
* bean capable of processing Spring's @{@link PulsarListener} annotation. Also register
* a default {@link PulsarListenerEndpointRegistry}.
*
* <p>This configuration class is automatically imported when using the @{@link EnablePulsar}
* annotation.
*
* @author Soby Chacko
*
* @see PulsarListenerAnnotationBeanPostProcessor
* @see PulsarListenerEndpointRegistry
* @see EnablePulsar
*/
public class PulsarBootstrapConfiguration implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(
PulsarListenerConfigUtils.PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)) {
registry.registerBeanDefinition(PulsarListenerConfigUtils.PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME,
new RootBeanDefinition(PulsarListenerAnnotationBeanPostProcessor.class));
}
if (!registry.containsBeanDefinition(PulsarListenerConfigUtils.PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)) {
registry.registerBeanDefinition(PulsarListenerConfigUtils.PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,
new RootBeanDefinition(PulsarListenerEndpointRegistry.class));
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.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.messaging.handler.annotation.MessageMapping;
/**
* Annotation that marks a method to be the target of a Pulsar message listener on the
* specified topics.
*
* The {@link #containerFactory()} identifies the
* {@link org.springframework.pulsar.config.PulsarListenerContainerFactory} to use to build the Pulsar listener container.
* If not set, a <em>default</em> container factory is assumed to be available with a bean name
* of {@code pulsarListenerContainerFactory} unless an explicit default has been provided
* through configuration.
*
* <p>
* Processing of {@code @PulsarListener} annotations is performed by registering a
* {@link PulsarListenerAnnotationBeanPostProcessor}. This can be done manually or, more
* conveniently, through {@link EnablePulsar} annotation.
*
* <p>
*
* @author Soby Chacko
*/
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@MessageMapping
@Documented
public @interface PulsarListener {
/**
* The unique identifier of the container for this listener.
* <p>If none is specified an auto-generated id is used.
* <p>SpEL {@code #{...}} and property place holders {@code ${...}} are supported.
* @return the {@code id} for the container managing for this endpoint.
* @see org.springframework.pulsar.config.PulsarListenerEndpointRegistry#getListenerContainer(String)
*/
String id() default "";
/**
* Pulsar subscription name associated with this listener.
* @return the {@code subscriptionName} for this Pulsar listener endpoint.
*/
String subscriptionName() default "";
/**
* Pulsar subscription type for this listener.
* @return the {@code subscriptionType} for this listener
*/
String subscriptionType() default "";
/**
* Specific container factory to use on this listener.
* @return {@code containerFactory} to use on this Pulsar listener.
*/
String containerFactory() default "";
/**
* Topics to listen to.
*
* @return a comma separated list of topics to listen from.
*/
String[] topics() default {};
/**
* Topic patten to listen to.
*
* @return topic pattern to listen to.
*/
String topicPattern() default "";
/**
* Set to true or false, to override the default setting in the container factory. May
* be a property placeholder or SpEL expression that evaluates to a {@link Boolean} or
* a {@link String}, in which case the {@link Boolean#parseBoolean(String)} is used to
* obtain the value.
* <p>SpEL {@code #{...}} and property place holders {@code ${...}} are supported.
* @return true to auto start, false to not auto start.
*/
String autoStartup() default "";
/**
* Activate batch consumption.
*
* @return whether this listener is in batch mode or not.
*/
String batch() default "";
/**
* A pseudo bean name used in SpEL expressions within this annotation to reference
* the current bean within which this listener is defined. This allows access to
* properties and methods within the enclosing bean.
* Default '__listener'.
* <p>
* @return the pseudo bean name.
*/
String beanRef() default "__listener";
String[] properties() default {};
}

View File

@@ -0,0 +1,801 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.annotation;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.log.LogAccessor;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.converter.GenericMessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.pulsar.config.MethodPulsarListenerEndpoint;
import org.springframework.pulsar.config.PulsarListenerConfigUtils;
import org.springframework.pulsar.config.PulsarListenerContainerFactory;
import org.springframework.pulsar.config.PulsarListenerEndpointRegistrar;
import org.springframework.pulsar.config.PulsarListenerEndpointRegistry;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
/**
* Bean post-processor that registers methods annotated with {@link PulsarListener}
* to be invoked by a Pulsar message listener container created under the covers
* by a {@link org.springframework.pulsar.config.PulsarListenerContainerFactory}
* according to the parameters of the annotation.
*
* <p>Annotated methods can use flexible arguments as defined by {@link PulsarListener}.
*
* <p>This post-processor is automatically registered by Spring's {@link EnablePulsar}
* annotation.
*
* <p>Auto-detect any {@link PulsarListenerConfigurer} instances in the container,
* allowing for customization of the registry to be used, the default container
* factory or for fine-grained control over endpoints registration. See
* {@link EnablePulsar} Javadoc for complete usage details.
*
* @param <K> the key type.
* @param <V> the value type.
*
* @author Soby Chacko
*
* @see PulsarListener
* @see EnablePulsar
* @see PulsarListenerConfigurer
* @see PulsarListenerEndpointRegistrar
* @see PulsarListenerEndpointRegistry
* @see org.springframework.pulsar.config.PulsarListenerEndpoint
* @see MethodPulsarListenerEndpoint
*/
public class PulsarListenerAnnotationBeanPostProcessor<K, V> implements BeanPostProcessor, Ordered, ApplicationContextAware, InitializingBean, SmartInitializingSingleton {
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
public static final String DEFAULT_PULSAR_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "pulsarListenerContainerFactory";
private static final String THE_LEFT = "The [";
private static final String RESOLVED_TO_LEFT = "Resolved to [";
private static final String RIGHT_FOR_LEFT = "] for [";
private static final String GENERATED_ID_PREFIX = "org.springframework.Pulsar.PulsarListenerEndpointContainer#";
private ApplicationContext applicationContext;
private BeanFactory beanFactory;
private BeanExpressionResolver resolver;
private BeanExpressionContext expressionContext;
private PulsarListenerEndpointRegistry endpointRegistry;
private String defaultContainerFactoryBeanName = DEFAULT_PULSAR_LISTENER_CONTAINER_FACTORY_BEAN_NAME;
private final PulsarListenerEndpointRegistrar registrar = new PulsarListenerEndpointRegistrar();
private final PulsarHandlerMethodFactoryAdapter messageHandlerMethodFactory =
new PulsarHandlerMethodFactoryAdapter();
private Charset charset = StandardCharsets.UTF_8;
private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));
private final ListenerScope listenerScope = new ListenerScope();
private AnnotationEnhancer enhancer;
private final AtomicInteger counter = new AtomicInteger();
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
public void setEndpointRegistry(PulsarListenerEndpointRegistry endpointRegistry) {
this.endpointRegistry = endpointRegistry;
}
public void setDefaultContainerFactoryBeanName(String containerFactoryBeanName) {
this.defaultContainerFactoryBeanName = containerFactoryBeanName;
}
public void setCharset(Charset charset) {
Assert.notNull(charset, "'charset' cannot be null");
this.charset = charset;
}
@Override
public void afterPropertiesSet() throws Exception {
buildEnhancer();
}
private void buildEnhancer() {
if (this.applicationContext != null) {
Map<String, AnnotationEnhancer> enhancersMap =
this.applicationContext.getBeansOfType(AnnotationEnhancer.class, false, false);
if (enhancersMap.size() > 0) {
List<AnnotationEnhancer> enhancers = enhancersMap.values()
.stream()
.sorted(new OrderComparator())
.collect(Collectors.toList());
this.enhancer = (attrs, element) -> {
Map<String, Object> newAttrs = attrs;
for (AnnotationEnhancer enh : enhancers) {
newAttrs = enh.apply(newAttrs, element);
}
return attrs;
};
}
}
}
@Override
public void afterSingletonsInstantiated() {
this.registrar.setBeanFactory(this.beanFactory);
if (this.beanFactory instanceof ListableBeanFactory) {
Map<String, PulsarListenerConfigurer> instances =
((ListableBeanFactory) this.beanFactory).getBeansOfType(PulsarListenerConfigurer.class);
for (PulsarListenerConfigurer configurer : instances.values()) {
configurer.configurePulsarListeners(this.registrar);
}
}
if (this.registrar.getEndpointRegistry() == null) {
if (this.endpointRegistry == null) {
Assert.state(this.beanFactory != null,
"BeanFactory must be set to find endpoint registry by bean name");
this.endpointRegistry = this.beanFactory.getBean(
PulsarListenerConfigUtils.PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,
PulsarListenerEndpointRegistry.class);
}
this.registrar.setEndpointRegistry(this.endpointRegistry);
}
if (this.defaultContainerFactoryBeanName != null) {
this.registrar.setContainerFactoryBeanName(this.defaultContainerFactoryBeanName);
}
// Set the custom handler method factory once resolved by the configurer
MessageHandlerMethodFactory handlerMethodFactory = this.registrar.getMessageHandlerMethodFactory();
if (handlerMethodFactory != null) {
this.messageHandlerMethodFactory.setHandlerMethodFactory(handlerMethodFactory);
}
else {
addFormatters(this.messageHandlerMethodFactory.defaultFormattingConversionService);
}
// Actually register all listeners
this.registrar.afterPropertiesSet();
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
if (!this.nonAnnotatedClasses.contains(bean.getClass())) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
Collection<PulsarListener> classLevelListeners = findListenerAnnotations(targetClass);
Map<Method, Set<PulsarListener>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<PulsarListener>>) method -> {
Set<PulsarListener> listenerMethods = findListenerAnnotations(method);
return (!listenerMethods.isEmpty() ? listenerMethods : null);
});
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(bean.getClass());
this.logger.trace(() -> "No @PulsarListener annotations found on bean type: " + bean.getClass());
}
else {
// Non-empty set of methods
for (Map.Entry<Method, Set<PulsarListener>> entry : annotatedMethods.entrySet()) {
Method method = entry.getKey();
for (PulsarListener listener : entry.getValue()) {
processPulsarListener(listener, method, bean, beanName);
}
}
this.logger.debug(() -> annotatedMethods.size() + " @PulsarListener methods processed on bean '"
+ beanName + "': " + annotatedMethods);
}
}
return bean;
}
protected void processPulsarListener(PulsarListener pulsarListener, Method method, Object bean, String beanName) {
Method methodToUse = checkProxy(method, bean);
MethodPulsarListenerEndpoint<V> endpoint = new MethodPulsarListenerEndpoint<>();
endpoint.setMethod(methodToUse);
String beanRef = pulsarListener.beanRef();
this.listenerScope.addListener(beanRef, bean);
String[] topics = resolveTopics(pulsarListener);
processListener(endpoint, pulsarListener, bean, beanName, topics);
this.listenerScope.removeListener(beanRef);
}
protected void processListener(MethodPulsarListenerEndpoint<?> endpoint, PulsarListener PulsarListener,
Object bean, String beanName, String[] topics) {
processPulsarListenerAnnotation(endpoint, PulsarListener, bean, topics);
String containerFactory = resolve(PulsarListener.containerFactory());
PulsarListenerContainerFactory<?> listenerContainerFactory = resolveContainerFactory(PulsarListener,
containerFactory, beanName);
this.registrar.registerEndpoint(endpoint, listenerContainerFactory);
}
@Nullable
private PulsarListenerContainerFactory<?> resolveContainerFactory(PulsarListener PulsarListener,
Object factoryTarget, String beanName) {
String containerFactory = PulsarListener.containerFactory();
if (!StringUtils.hasText(containerFactory)) {
return null;
}
PulsarListenerContainerFactory<?> factory = null;
Object resolved = resolveExpression(containerFactory);
if (resolved instanceof PulsarListenerContainerFactory) {
return (PulsarListenerContainerFactory<?>) resolved;
}
String containerFactoryBeanName = resolveExpressionAsString(containerFactory,
"containerFactory");
if (StringUtils.hasText(containerFactoryBeanName)) {
assertBeanFactory();
try {
factory = this.beanFactory.getBean(containerFactoryBeanName, PulsarListenerContainerFactory.class);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanInitializationException(
noBeanFoundMessage(factoryTarget, beanName, containerFactoryBeanName,
PulsarListenerContainerFactory.class), ex);
}
}
return factory;
}
protected void assertBeanFactory() {
Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");
}
protected String noBeanFoundMessage(Object target, String listenerBeanName, String requestedBeanName,
Class<?> expectedClass) {
return "Could not register Pulsar listener endpoint on ["
+ target + "] for bean " + listenerBeanName + ", no '" + expectedClass.getSimpleName() + "' with id '"
+ requestedBeanName + "' was found in the application context";
}
private void processPulsarListenerAnnotation(MethodPulsarListenerEndpoint<?> endpoint,
PulsarListener pulsarListener, Object bean, String[] topics) {
endpoint.setBean(bean);
endpoint.setMessageHandlerMethodFactory(this.messageHandlerMethodFactory);
endpoint.setSubscriptionName(getEndpointSubscriptionName(pulsarListener));
endpoint.setId(getEndpointId(pulsarListener));
endpoint.setTopics(topics);
endpoint.setSubscriptionType(getEndpointSubscriptionType(pulsarListener));
String autoStartup = pulsarListener.autoStartup();
if (StringUtils.hasText(autoStartup)) {
endpoint.setAutoStartup(resolveExpressionAsBoolean(autoStartup, "autoStartup"));
}
resolvePulsarProperties(endpoint, pulsarListener.properties());
if (StringUtils.hasText(pulsarListener.batch())) {
endpoint.setBatchListener(Boolean.parseBoolean(pulsarListener.batch()));
}
endpoint.setBeanFactory(this.beanFactory);
}
private Boolean resolveExpressionAsBoolean(String value, String attribute) {
Object resolved = resolveExpression(value);
Boolean result = null;
if (resolved instanceof Boolean) {
result = (Boolean) resolved;
}
else if (resolved instanceof String) {
result = Boolean.parseBoolean((String) resolved);
}
else if (resolved != null) {
throw new IllegalStateException(
THE_LEFT + attribute + "] must resolve to a Boolean or a String that can be parsed as a Boolean. "
+ RESOLVED_TO_LEFT + resolved.getClass() + RIGHT_FOR_LEFT + value + "]");
}
return result;
}
private void resolvePulsarProperties(MethodPulsarListenerEndpoint<?> endpoint, String[] propertyStrings) {
if (propertyStrings.length > 0) {
Properties properties = new Properties();
for (String property : propertyStrings) {
Object value = resolveExpression(property);
if (value instanceof String) {
loadProperty(properties, property, value);
}
else if (value instanceof String[]) {
for (String prop : (String[]) value) {
loadProperty(properties, prop, prop);
}
}
else if (value instanceof Collection) {
Collection<?> values = (Collection<?>) value;
if (values.size() > 0 && values.iterator().next() instanceof String) {
for (String prop : (Collection<String>) value) {
loadProperty(properties, prop, prop);
}
}
}
else {
throw new IllegalStateException("'properties' must resolve to a String, a String[] or "
+ "Collection<String>");
}
}
endpoint.setConsumerProperties(properties);
}
}
private void loadProperty(Properties properties, String property, Object value) {
try {
properties.load(new StringReader((String) value));
}
catch (IOException e) {
this.logger.error(e, () -> "Failed to load property " + property + ", continuing...");
}
}
private String getEndpointSubscriptionName(PulsarListener pulsarListener) {
if (StringUtils.hasText(pulsarListener.subscriptionName())) {
return resolveExpressionAsString(pulsarListener.subscriptionName(), "subscriptionName");
}
else {
return GENERATED_ID_PREFIX + this.counter.getAndIncrement();
}
}
private SubscriptionType getEndpointSubscriptionType(PulsarListener pulsarListener) {
final String subscriptionType = pulsarListener.subscriptionType().toLowerCase();
if (StringUtils.hasText(subscriptionType)) {
return switch (subscriptionType) {
case "exclusive" -> SubscriptionType.Exclusive;
case "failover" -> SubscriptionType.Failover;
case "shared" -> SubscriptionType.Shared;
case "key_shared" -> SubscriptionType.Key_Shared;
default -> SubscriptionType.Exclusive;
};
}
return null;
}
private String getEndpointId(PulsarListener pulsarListener) {
if (StringUtils.hasText(pulsarListener.id())) {
return resolveExpressionAsString(pulsarListener.id(), "id");
}
else {
return GENERATED_ID_PREFIX + this.counter.getAndIncrement();
}
}
private String resolveExpressionAsString(String value, String attribute) {
Object resolved = resolveExpression(value);
if (resolved instanceof String) {
return (String) resolved;
}
else if (resolved != null) {
throw new IllegalStateException(THE_LEFT + attribute + "] must resolve to a String. "
+ RESOLVED_TO_LEFT + resolved.getClass() + RIGHT_FOR_LEFT + value + "]");
}
return null;
}
private String[] resolveTopics(PulsarListener PulsarListener) {
String[] topics = PulsarListener.topics();
List<String> result = new ArrayList<>();
if (topics.length > 0) {
for (String topic1 : topics) {
Object topic = resolveExpression(topic1);
resolveAsString(topic, result);
}
}
return result.toArray(new String[0]);
}
private Object resolveExpression(String value) {
return this.resolver.evaluate(resolve(value), this.expressionContext);
}
private String resolve(String value) {
if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory) {
return ((ConfigurableBeanFactory) this.beanFactory).resolveEmbeddedValue(value);
}
return value;
}
private void resolveAsString(Object resolvedValue, List<String> result) {
if (resolvedValue instanceof String[]) {
for (Object object : (String[]) resolvedValue) {
resolveAsString(object, result);
}
}
else if (resolvedValue instanceof String) {
result.add((String) resolvedValue);
}
else if (resolvedValue instanceof Iterable) {
for (Object object : (Iterable<Object>) resolvedValue) {
resolveAsString(object, result);
}
}
else {
throw new IllegalArgumentException(String.format(
"@PulsarListener can't resolve '%s' as a String", resolvedValue));
}
}
private Method checkProxy(Method methodArg, Object bean) {
Method method = methodArg;
if (AopUtils.isJdkDynamicProxy(bean)) {
try {
// Found a @PulsarListener method on the target class for this JDK proxy ->
// is it also present on the proxy itself?
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
Class<?>[] proxiedInterfaces = ((Advised) bean).getProxiedInterfaces();
for (Class<?> iface : proxiedInterfaces) {
try {
method = iface.getMethod(method.getName(), method.getParameterTypes());
break;
}
catch (@SuppressWarnings("unused") NoSuchMethodException noMethod) {
// NOSONAR
}
}
}
catch (SecurityException ex) {
ReflectionUtils.handleReflectionException(ex);
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException(String.format(
"@PulsarListener method '%s' found on bean target class '%s', " +
"but not found in any interface(s) for bean JDK proxy. Either " +
"pull the method up to an interface or switch to subclass (CGLIB) " +
"proxies by setting proxy-target-class/proxyTargetClass " +
"attribute to 'true'", method.getName(),
method.getDeclaringClass().getSimpleName()), ex);
}
}
return method;
}
private Collection<PulsarListener> findListenerAnnotations(Class<?> clazz) {
Set<PulsarListener> listeners = new HashSet<>();
PulsarListener ann = AnnotatedElementUtils.findMergedAnnotation(clazz, PulsarListener.class);
if (ann != null) {
ann = enhance(clazz, ann);
listeners.add(ann);
}
PulsarListeners anns = AnnotationUtils.findAnnotation(clazz, PulsarListeners.class);
if (anns != null) {
listeners.addAll(Arrays.stream(anns.value())
.map(anno -> enhance(clazz, anno))
.collect(Collectors.toList()));
}
return listeners;
}
private Set<PulsarListener> findListenerAnnotations(Method method) {
Set<PulsarListener> listeners = new HashSet<>();
PulsarListener ann = AnnotatedElementUtils.findMergedAnnotation(method, PulsarListener.class);
if (ann != null) {
ann = enhance(method, ann);
listeners.add(ann);
}
PulsarListeners anns = AnnotationUtils.findAnnotation(method, PulsarListeners.class);
if (anns != null) {
listeners.addAll(Arrays.stream(anns.value())
.map(anno -> enhance(method, anno))
.collect(Collectors.toList()));
}
return listeners;
}
private PulsarListener enhance(AnnotatedElement element, PulsarListener ann) {
if (this.enhancer == null) {
return ann;
}
else {
return AnnotationUtils.synthesizeAnnotation(
this.enhancer.apply(AnnotationUtils.getAnnotationAttributes(ann), element), PulsarListener.class, null);
}
}
private void addFormatters(FormatterRegistry registry) {
for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
registry.addConverter(converter);
}
for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
registry.addConverter(converter);
}
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
}
private <T> Collection<T> getBeansOfType(Class<T> type) {
if (PulsarListenerAnnotationBeanPostProcessor.this.beanFactory instanceof ListableBeanFactory) {
return ((ListableBeanFactory) PulsarListenerAnnotationBeanPostProcessor.this.beanFactory)
.getBeansOfType(type)
.values();
}
else {
return Collections.emptySet();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
if (applicationContext instanceof ConfigurableApplicationContext) {
setBeanFactory(((ConfigurableApplicationContext) applicationContext).getBeanFactory());
}
else {
setBeanFactory(applicationContext);
}
}
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.resolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver();
this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory,
this.listenerScope);
}
}
private class PulsarHandlerMethodFactoryAdapter implements MessageHandlerMethodFactory {
private final DefaultFormattingConversionService defaultFormattingConversionService =
new DefaultFormattingConversionService();
private MessageHandlerMethodFactory handlerMethodFactory;
public void setHandlerMethodFactory(MessageHandlerMethodFactory pulsarHandlerMethodFactory1) {
this.handlerMethodFactory = pulsarHandlerMethodFactory1;
}
@Override
public InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method) {
return getHandlerMethodFactory().createInvocableHandlerMethod(bean, method);
}
private MessageHandlerMethodFactory getHandlerMethodFactory() {
if (this.handlerMethodFactory == null) {
this.handlerMethodFactory = createDefaultMessageHandlerMethodFactory();
}
return this.handlerMethodFactory;
}
private MessageHandlerMethodFactory createDefaultMessageHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory defaultFactory = new DefaultMessageHandlerMethodFactory();
Validator validator = PulsarListenerAnnotationBeanPostProcessor.this.registrar.getValidator();
if (validator != null) {
defaultFactory.setValidator(validator);
}
defaultFactory.setBeanFactory(PulsarListenerAnnotationBeanPostProcessor.this.beanFactory);
this.defaultFormattingConversionService.addConverter(
new BytesToStringConverter(PulsarListenerAnnotationBeanPostProcessor.this.charset));
this.defaultFormattingConversionService.addConverter(new BytesToNumberConverter());
defaultFactory.setConversionService(this.defaultFormattingConversionService);
GenericMessageConverter messageConverter = new GenericMessageConverter(this.defaultFormattingConversionService);
defaultFactory.setMessageConverter(messageConverter);
List<HandlerMethodArgumentResolver> customArgumentsResolver =
new ArrayList<>(PulsarListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers());
// Has to be at the end - look at PayloadMethodArgumentResolver documentation
//customArgumentsResolver.add(new PulsarNullAwarePayloadArgumentResolver(messageConverter, validator));
defaultFactory.setCustomArgumentResolvers(customArgumentsResolver);
defaultFactory.afterPropertiesSet();
return defaultFactory;
}
}
private static class BytesToStringConverter implements Converter<byte[], String> {
private final Charset charset;
BytesToStringConverter(Charset charset) {
this.charset = charset;
}
@Override
public String convert(byte[] source) {
return new String(source, this.charset);
}
}
private final class BytesToNumberConverter implements ConditionalGenericConverter {
BytesToNumberConverter() {
}
@Override
@Nullable
public Set<ConvertiblePair> getConvertibleTypes() {
HashSet<ConvertiblePair> pairs = new HashSet<>();
pairs.add(new ConvertiblePair(byte[].class, long.class));
pairs.add(new ConvertiblePair(byte[].class, int.class));
pairs.add(new ConvertiblePair(byte[].class, short.class));
pairs.add(new ConvertiblePair(byte[].class, byte.class));
pairs.add(new ConvertiblePair(byte[].class, Long.class));
pairs.add(new ConvertiblePair(byte[].class, Integer.class));
pairs.add(new ConvertiblePair(byte[].class, Short.class));
pairs.add(new ConvertiblePair(byte[].class, Byte.class));
return pairs;
}
@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
byte[] bytes = (byte[]) source;
if (targetType.getType().equals(long.class) || targetType.getType().equals(Long.class)) {
Assert.state(bytes.length >= 8, "At least 8 bytes needed to convert a byte[] to a long"); // NOSONAR
return ByteBuffer.wrap(bytes).getLong();
}
else if (targetType.getType().equals(int.class) || targetType.getType().equals(Integer.class)) {
Assert.state(bytes.length >= 4, "At least 4 bytes needed to convert a byte[] to an integer"); // NOSONAR
return ByteBuffer.wrap(bytes).getInt();
}
else if (targetType.getType().equals(short.class) || targetType.getType().equals(Short.class)) {
Assert.state(bytes.length >= 2, "At least 2 bytes needed to convert a byte[] to a short");
return ByteBuffer.wrap(bytes).getShort();
}
else if (targetType.getType().equals(byte.class) || targetType.getType().equals(Byte.class)) {
Assert.state(bytes.length >= 1, "At least 1 byte needed to convert a byte[] to a byte");
return ByteBuffer.wrap(bytes).get();
}
return null;
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType.getType().equals(byte[].class)) {
Class<?> target = targetType.getType();
return target.equals(long.class) || target.equals(int.class) || target.equals(short.class) // NOSONAR
|| target.equals(byte.class) || target.equals(Long.class) || target.equals(Integer.class)
|| target.equals(Short.class) || target.equals(Byte.class);
}
else {
return false;
}
}
}
static class ListenerScope implements Scope {
private final Map<String, Object> listeners = new HashMap<>();
ListenerScope() {
}
public void addListener(String key, Object bean) {
this.listeners.put(key, bean);
}
public void removeListener(String key) {
this.listeners.remove(key);
}
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return this.listeners.get(name);
}
@Override
public Object remove(String name) {
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return this.listeners.get(key);
}
@Override
public String getConversationId() {
return null;
}
}
public interface AnnotationEnhancer extends BiFunction<Map<String, Object>, AnnotatedElement, Map<String, Object>> {
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.annotation;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotationMetadata;
/**
* A {@link DeferredImportSelector} implementation with the lowest order to import a
* {@link PulsarBootstrapConfiguration} as late as possible.
*
* @author Soby Chacko
*
*/
@Order
public class PulsarListenerConfigurationSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] { PulsarBootstrapConfiguration.class.getName() };
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.annotation;
import org.springframework.pulsar.config.PulsarListenerEndpointRegistrar;
/**
* @author Soby Chacko
*/
public interface PulsarListenerConfigurer {
void configurePulsarListeners(PulsarListenerEndpointRegistrar registrar);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.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.pulsar.annotation.PulsarListener;
/**
* @author Soby Chacko
*/
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PulsarListeners {
PulsarListener[] value();
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.autoconfig;
import org.apache.pulsar.client.api.Schema;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.pulsar.annotation.EnablePulsar;
import org.springframework.pulsar.config.PulsarListenerConfigUtils;
import org.springframework.pulsar.config.PulsarListenerContainerFactoryImpl;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.listener.PulsarContainerProperties;
/**
* @author Soby Chacko
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnablePulsar.class)
public class PulsarAnnotationDrivenConfiguration {
private final PulsarProperties pulsarProperties;
public PulsarAnnotationDrivenConfiguration(PulsarProperties pulsarProperties) {
this.pulsarProperties = pulsarProperties;
}
@Bean
@ConditionalOnMissingBean(name = "pulsarListenerContainerFactory")
PulsarListenerContainerFactoryImpl<?, ?> pulsarListenerContainerFactory(
ObjectProvider<PulsarConsumerFactory<Object>> pulsarConsumerFactory) {
PulsarListenerContainerFactoryImpl<Object, Object> factory = new PulsarListenerContainerFactoryImpl<>();
final PulsarConsumerFactory<Object> pulsarConsumerFactory1 = pulsarConsumerFactory.getIfAvailable();
factory.setPulsarConsumerFactory(pulsarConsumerFactory1);
final PulsarContainerProperties containerProperties = factory.getContainerProperties();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
PulsarProperties.Listener properties = this.pulsarProperties.getListener();
map.from(properties::getSchema).as(
schema1 -> switch (schema1) {
case STRING -> Schema.STRING;
case BYTES -> Schema.BYTES;
case BYTEBUFFER -> Schema.BYTEBUFFER;
}).to(containerProperties::setSchema);
return factory;
}
@Configuration(proxyBeanMethods = false)
@EnablePulsar
@ConditionalOnMissingBean(name = PulsarListenerConfigUtils.PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
static class EnableKafkaConfiguration {
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.autoconfig;
import org.apache.pulsar.client.api.PulsarClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
import org.springframework.pulsar.config.PulsarClientConfiguration;
import org.springframework.pulsar.config.PulsarClientFactoryBean;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.core.PulsarProducerFactory;
import org.springframework.pulsar.core.PulsarTemplate;
/**
* @author Soby Chacko
*/
@AutoConfiguration
@ConditionalOnClass(PulsarTemplate.class)
@EnableConfigurationProperties(PulsarProperties.class)
@Import({ PulsarAnnotationDrivenConfiguration.class })
public class PulsarAutoConfiguration {
private final PulsarProperties properties;
public PulsarAutoConfiguration(PulsarProperties properties) {
this.properties = properties;
}
@Bean
public PulsarClientFactoryBean pulsarClientFactoryBean(PulsarClientConfiguration pulsarClientConfiguration) {
return new PulsarClientFactoryBean(pulsarClientConfiguration);
}
@Bean
public PulsarClientConfiguration pulsarClientConfiguration() {
return new PulsarClientConfiguration(this.properties.buildClientProperties());
}
@Bean
public PulsarProducerFactory<?> pulsarProducerFactory(PulsarClient pulsarClient) {
return new DefaultPulsarProducerFactory<>(pulsarClient, this.properties.buildProducerProperties());
}
@Bean
public PulsarTemplate<?> pulsarTemplate(PulsarProducerFactory<?> pulsarProducerFactory) {
return new PulsarTemplate<>(pulsarProducerFactory);
}
@Bean
@ConditionalOnMissingBean(PulsarConsumerFactory.class)
public PulsarConsumerFactory<?> pulsarConsumerFactory(PulsarClient pulsarClient) {
DefaultPulsarConsumerFactory<Object> factory = new DefaultPulsarConsumerFactory<>(pulsarClient,
this.properties.buildConsumerProperties());
return factory;
}
}

View File

@@ -0,0 +1,774 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.autoconfig;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.apache.pulsar.client.api.CompressionType;
import org.apache.pulsar.client.api.ConsumerCryptoFailureAction;
import org.apache.pulsar.client.api.HashingScheme;
import org.apache.pulsar.client.api.MessageRoutingMode;
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.SubscriptionType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
/**
* @author Soby Chacko
*/
@ConfigurationProperties(prefix = "spring.pulsar")
public class PulsarProperties {
private final Consumer consumer = new Consumer();
private final Client client = new Client();
private final Listener listener = new Listener();
private final Producer producer = new Producer();
public Map<String, Object> buildConsumerProperties() {
Map<String, Object> properties = new HashMap<>();
properties.putAll(this.consumer.buildProperties());
return properties;
}
public Map<String, Object> buildProducerProperties() {
Map<String, Object> properties = new HashMap<>();
properties.putAll(this.producer.buildProperties());
return properties;
}
public enum Schema {
STRING,
BYTES,
BYTEBUFFER;
}
public Consumer getConsumer() {
return consumer;
}
public Listener getListener() {
return listener;
}
public Client getClient() {
return client;
}
public Producer getProducer() {
return producer;
}
public Map<String, Object> buildClientProperties() {
return new HashMap<>(this.client.buildProperties());
}
public static class Consumer {
private String[] topics;
private String topicsPattern;
private String subscriptionName;
private SubscriptionType subscriptionType = SubscriptionType.Exclusive;
private int receiverQueueSize = 1000;
private long acknowledgementsGroupTimeMicros = TimeUnit.MILLISECONDS.toMicros(100);
private long negativeAckRedeliveryDelayMicros = TimeUnit.MINUTES.toMicros(1);
private int maxTotalReceiverQueueSizeAcrossPartitions = 50000;
private String consumerName;
private long ackTimeoutMillis = 0;
private long tickDurationMillis = 1000;
private int priorityLevel = 0;
private ConsumerCryptoFailureAction cryptoFailureAction = ConsumerCryptoFailureAction.FAIL;
private SortedMap<String, String> properties = new TreeMap<>();
private boolean readCompacted = false;
private SubscriptionInitialPosition subscriptionInitialPosition = SubscriptionInitialPosition.Latest;
private int patternAutoDiscoveryPeriod = 1;
private RegexSubscriptionMode regexSubscriptionMode = RegexSubscriptionMode.PersistentOnly;
private boolean autoUpdatePartitions = true;
private boolean replicateSubscriptionState = false;
private boolean autoAckOldestChunkedMessageOnQueueFull = true;
private int maxPendingChunkedMessage = 10;
private long expireTimeOfIncompleteChunkedMessageMillis = 60000;
public String[] getTopics() {
return topics;
}
public void setTopics(String[] topics) {
this.topics = topics;
}
public String getTopicsPattern() {
return topicsPattern;
}
public void setTopicsPattern(String topicsPattern) {
this.topicsPattern = topicsPattern;
}
public String getSubscriptionName() {
return subscriptionName;
}
public void setSubscriptionName(String subscriptionName) {
this.subscriptionName = subscriptionName;
}
public SubscriptionType getSubscriptionType() {
return subscriptionType;
}
public void setSubscriptionType(SubscriptionType subscriptionType) {
this.subscriptionType = subscriptionType;
}
public int getReceiverQueueSize() {
return receiverQueueSize;
}
public void setReceiverQueueSize(int receiverQueueSize) {
this.receiverQueueSize = receiverQueueSize;
}
public long getAcknowledgementsGroupTimeMicros() {
return acknowledgementsGroupTimeMicros;
}
public void setAcknowledgementsGroupTimeMicros(long acknowledgementsGroupTimeMicros) {
this.acknowledgementsGroupTimeMicros = acknowledgementsGroupTimeMicros;
}
public long getNegativeAckRedeliveryDelayMicros() {
return negativeAckRedeliveryDelayMicros;
}
public void setNegativeAckRedeliveryDelayMicros(long negativeAckRedeliveryDelayMicros) {
this.negativeAckRedeliveryDelayMicros = negativeAckRedeliveryDelayMicros;
}
public int getMaxTotalReceiverQueueSizeAcrossPartitions() {
return maxTotalReceiverQueueSizeAcrossPartitions;
}
public void setMaxTotalReceiverQueueSizeAcrossPartitions(int maxTotalReceiverQueueSizeAcrossPartitions) {
this.maxTotalReceiverQueueSizeAcrossPartitions = maxTotalReceiverQueueSizeAcrossPartitions;
}
public String getConsumerName() {
return consumerName;
}
public void setConsumerName(String consumerName) {
this.consumerName = consumerName;
}
public long getAckTimeoutMillis() {
return ackTimeoutMillis;
}
public void setAckTimeoutMillis(long ackTimeoutMillis) {
this.ackTimeoutMillis = ackTimeoutMillis;
}
public long getTickDurationMillis() {
return tickDurationMillis;
}
public void setTickDurationMillis(long tickDurationMillis) {
this.tickDurationMillis = tickDurationMillis;
}
public int getPriorityLevel() {
return priorityLevel;
}
public void setPriorityLevel(int priorityLevel) {
this.priorityLevel = priorityLevel;
}
public ConsumerCryptoFailureAction getCryptoFailureAction() {
return cryptoFailureAction;
}
public void setCryptoFailureAction(ConsumerCryptoFailureAction cryptoFailureAction) {
this.cryptoFailureAction = cryptoFailureAction;
}
public SortedMap<String, String> getProperties() {
return properties;
}
public void setProperties(SortedMap<String, String> properties) {
this.properties = properties;
}
public boolean isReadCompacted() {
return readCompacted;
}
public void setReadCompacted(boolean readCompacted) {
this.readCompacted = readCompacted;
}
public SubscriptionInitialPosition getSubscriptionInitialPosition() {
return subscriptionInitialPosition;
}
public void setSubscriptionInitialPosition(SubscriptionInitialPosition subscriptionInitialPosition) {
this.subscriptionInitialPosition = subscriptionInitialPosition;
}
public int getPatternAutoDiscoveryPeriod() {
return patternAutoDiscoveryPeriod;
}
public void setPatternAutoDiscoveryPeriod(int patternAutoDiscoveryPeriod) {
this.patternAutoDiscoveryPeriod = patternAutoDiscoveryPeriod;
}
public RegexSubscriptionMode getRegexSubscriptionMode() {
return regexSubscriptionMode;
}
public void setRegexSubscriptionMode(RegexSubscriptionMode regexSubscriptionMode) {
this.regexSubscriptionMode = regexSubscriptionMode;
}
public boolean isAutoUpdatePartitions() {
return autoUpdatePartitions;
}
public void setAutoUpdatePartitions(boolean autoUpdatePartitions) {
this.autoUpdatePartitions = autoUpdatePartitions;
}
public boolean isReplicateSubscriptionState() {
return replicateSubscriptionState;
}
public void setReplicateSubscriptionState(boolean replicateSubscriptionState) {
this.replicateSubscriptionState = replicateSubscriptionState;
}
public boolean isAutoAckOldestChunkedMessageOnQueueFull() {
return autoAckOldestChunkedMessageOnQueueFull;
}
public void setAutoAckOldestChunkedMessageOnQueueFull(boolean autoAckOldestChunkedMessageOnQueueFull) {
this.autoAckOldestChunkedMessageOnQueueFull = autoAckOldestChunkedMessageOnQueueFull;
}
public int getMaxPendingChunkedMessage() {
return maxPendingChunkedMessage;
}
public void setMaxPendingChunkedMessage(int maxPendingChunkedMessage) {
this.maxPendingChunkedMessage = maxPendingChunkedMessage;
}
public long getExpireTimeOfIncompleteChunkedMessageMillis() {
return expireTimeOfIncompleteChunkedMessageMillis;
}
public void setExpireTimeOfIncompleteChunkedMessageMillis(long expireTimeOfIncompleteChunkedMessageMillis) {
this.expireTimeOfIncompleteChunkedMessageMillis = expireTimeOfIncompleteChunkedMessageMillis;
}
public Map<String, Object> buildProperties() {
PulsarProperties.Properties properties = new Properties();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this::getTopics).as(Set::of).to(properties.in("topicNames"));
map.from(this::getTopicsPattern).as(Pattern::compile).to(properties.in("topicsPattern"));
map.from(this::getSubscriptionName).to(properties.in("subscriptionName"));
map.from(this::getSubscriptionType).to(properties.in("subscriptionType"));
map.from(this::getReceiverQueueSize)
.to(properties.in("receiverQueueSize"));
map.from(this::getAcknowledgementsGroupTimeMicros).to(properties.in("acknowledgementsGroupTimeMicros"));
map.from(this::getNegativeAckRedeliveryDelayMicros).to(properties.in("negativeAckRedeliveryDelayMicros"));
map.from(this::getMaxTotalReceiverQueueSizeAcrossPartitions).to(properties.in("maxTotalReceiverQueueSizeAcrossPartitions"));
map.from(this::getConsumerName).to(properties.in("consumerName"));
map.from(this::getAckTimeoutMillis).to(properties.in("ackTimeoutMillis"));
map.from(this::getTickDurationMillis).to(properties.in("tickDurationMillis"));
map.from(this::getPriorityLevel).to(properties.in("priorityLevel"));
map.from(this::getCryptoFailureAction).to(properties.in("cryptoFailureAction"));
map.from(this::getProperties).to(properties.in("properties"));
map.from(this::isReadCompacted).to(properties.in("readCompacted"));
map.from(this::getSubscriptionInitialPosition).to(properties.in("subscriptionInitialPosition"));
map.from(this::getPatternAutoDiscoveryPeriod).to(properties.in("patternAutoDiscoveryPeriod"));
map.from(this::getRegexSubscriptionMode).to(properties.in("regexSubscriptionMode"));
map.from(this::isAutoUpdatePartitions).to(properties.in("autoUpdatePartitions"));
map.from(this::isReplicateSubscriptionState).to(properties.in("replicateSubscriptionState"));
map.from(this::isAutoAckOldestChunkedMessageOnQueueFull).to(properties.in("autoAckOldestChunkedMessageOnQueueFull"));
map.from(this::getMaxPendingChunkedMessage).to(properties.in("maxPendingChunkedMessage"));
map.from(this::getExpireTimeOfIncompleteChunkedMessageMillis).to(properties.in("expireTimeOfIncompleteChunkedMessageMillis"));
return properties;
}
}
public static class Producer {
private String topicName;
private String producerName;
private long sendTimeoutMs = 30000;
private boolean blockIfQueueFull = false;
private int maxPendingMessages = 1000;
private int maxPendingMessagesAcrossPartitions = 50000;
private MessageRoutingMode messageRoutingMode = MessageRoutingMode.RoundRobinPartition;
private HashingScheme hashingScheme = HashingScheme.JavaStringHash;
private ProducerCryptoFailureAction cryptoFailureAction = ProducerCryptoFailureAction.FAIL;
private long batchingMaxPublishDelayMicros = TimeUnit.MILLISECONDS.toMicros(1);
private int batchingMaxMessages = 1000;
private boolean batchingEnabled = false;
private boolean chunkingEnabled = false;
private CompressionType compressionType;
private String initialSubscriptionName;
public String getTopicName() {
return topicName;
}
public void setTopicName(String topicName) {
this.topicName = topicName;
}
public String getProducerName() {
return producerName;
}
public void setProducerName(String producerName) {
this.producerName = producerName;
}
public long getSendTimeoutMs() {
return sendTimeoutMs;
}
public void setSendTimeoutMs(long sendTimeoutMs) {
this.sendTimeoutMs = sendTimeoutMs;
}
public boolean isBlockIfQueueFull() {
return blockIfQueueFull;
}
public void setBlockIfQueueFull(boolean blockIfQueueFull) {
this.blockIfQueueFull = blockIfQueueFull;
}
public int getMaxPendingMessages() {
return maxPendingMessages;
}
public void setMaxPendingMessages(int maxPendingMessages) {
this.maxPendingMessages = maxPendingMessages;
}
public int getMaxPendingMessagesAcrossPartitions() {
return maxPendingMessagesAcrossPartitions;
}
public void setMaxPendingMessagesAcrossPartitions(int maxPendingMessagesAcrossPartitions) {
this.maxPendingMessagesAcrossPartitions = maxPendingMessagesAcrossPartitions;
}
public MessageRoutingMode getMessageRoutingMode() {
return messageRoutingMode;
}
public void setMessageRoutingMode(MessageRoutingMode messageRoutingMode) {
this.messageRoutingMode = messageRoutingMode;
}
public HashingScheme getHashingScheme() {
return hashingScheme;
}
public void setHashingScheme(HashingScheme hashingScheme) {
this.hashingScheme = hashingScheme;
}
public ProducerCryptoFailureAction getCryptoFailureAction() {
return cryptoFailureAction;
}
public void setCryptoFailureAction(ProducerCryptoFailureAction cryptoFailureAction) {
this.cryptoFailureAction = cryptoFailureAction;
}
public long getBatchingMaxPublishDelayMicros() {
return batchingMaxPublishDelayMicros;
}
public void setBatchingMaxPublishDelayMicros(long batchingMaxPublishDelayMicros) {
this.batchingMaxPublishDelayMicros = batchingMaxPublishDelayMicros;
}
public int getBatchingMaxMessages() {
return batchingMaxMessages;
}
public void setBatchingMaxMessages(int batchingMaxMessages) {
this.batchingMaxMessages = batchingMaxMessages;
}
public boolean isBatchingEnabled() {
return batchingEnabled;
}
public void setBatchingEnabled(boolean batchingEnabled) {
this.batchingEnabled = batchingEnabled;
}
public boolean isChunkingEnabled() {
return chunkingEnabled;
}
public void setChunkingEnabled(boolean chunkingEnabled) {
this.chunkingEnabled = chunkingEnabled;
}
public CompressionType getCompressionType() {
return compressionType;
}
public void setCompressionType(CompressionType compressionType) {
this.compressionType = compressionType;
}
public String getInitialSubscriptionName() {
return initialSubscriptionName;
}
public void setInitialSubscriptionName(String initialSubscriptionName) {
this.initialSubscriptionName = initialSubscriptionName;
}
public Map<String, Object> buildProperties() {
PulsarProperties.Properties properties = new Properties();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this::getTopicName).to(properties.in("topicName"));
map.from(this::getProducerName).to(properties.in("producerName"));
map.from(this::getSendTimeoutMs).to(properties.in("sendTimeoutMs"));
map.from(this::isBlockIfQueueFull).to(properties.in("blockIfQueueFull"));
map.from(this::getMaxPendingMessages).to(properties.in("maxPendingMessages"));
map.from(this::getMaxPendingMessagesAcrossPartitions).to(properties.in("maxPendingMessagesAcrossPartitions"));
map.from(this::getMessageRoutingMode).to(properties.in("messageRoutingMode"));
map.from(this::getHashingScheme).to(properties.in("hashingScheme"));
map.from(this::getCryptoFailureAction).to(properties.in("cryptoFailureAction"));
map.from(this::getBatchingMaxPublishDelayMicros).to(properties.in("batchingMaxPublishDelayMicros"));
map.from(this::getBatchingMaxMessages).to(properties.in("batchingMaxMessages"));
map.from(this::isBatchingEnabled).to(properties.in("batchingEnabled"));
map.from(this::isChunkingEnabled).to(properties.in("chunkingEnabled"));
map.from(this::getCompressionType).to(properties.in("compressionType"));
map.from(this::getInitialSubscriptionName).to(properties.in("initialSubscriptionName"));
return properties;
}
}
public static class Client {
private String serviceUrl;
private String authPluginClassName;
private String authParams;
private long operationTimeoutMs = 30000L;
private long statsIntervalSeconds = 60;
private int numIoThreads = 1;
private boolean useTcpNoDelay = true;
private boolean useTls = false;
private String tlsTrustCertsFilePath;
private boolean tlsAllowInsecureConnection = false;
private boolean tlsHostnameVerificationEnable = false;
private int concurrentLookupRequest = 5000;
private int maxLookupRequest = 50000;
private int maxNumberOfRejectedRequestPerConnection = 50;
private int keepAliveIntervalSeconds = 30;
private int connectionTimeoutMs = 10000;
private int requestTimeoutMs = 60000;
private long initialBackoffIntervalNanos = TimeUnit.MILLISECONDS.toNanos(100);;
private long maxBackoffIntervalNanos = TimeUnit.SECONDS.toNanos(30);
public String getServiceUrl() {
return serviceUrl;
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
public String getAuthPluginClassName() {
return authPluginClassName;
}
public void setAuthPluginClassName(String authPluginClassName) {
this.authPluginClassName = authPluginClassName;
}
public String getAuthParams() {
return authParams;
}
public void setAuthParams(String authParams) {
this.authParams = authParams;
}
public long getOperationTimeoutMs() {
return operationTimeoutMs;
}
public void setOperationTimeoutMs(long operationTimeoutMs) {
this.operationTimeoutMs = operationTimeoutMs;
}
public long getStatsIntervalSeconds() {
return statsIntervalSeconds;
}
public void setStatsIntervalSeconds(long statsIntervalSeconds) {
this.statsIntervalSeconds = statsIntervalSeconds;
}
public int getNumIoThreads() {
return numIoThreads;
}
public void setNumIoThreads(int numIoThreads) {
this.numIoThreads = numIoThreads;
}
public boolean isUseTcpNoDelay() {
return useTcpNoDelay;
}
public void setUseTcpNoDelay(boolean useTcpNoDelay) {
this.useTcpNoDelay = useTcpNoDelay;
}
public boolean isUseTls() {
return useTls;
}
public void setUseTls(boolean useTls) {
this.useTls = useTls;
}
public String getTlsTrustCertsFilePath() {
return tlsTrustCertsFilePath;
}
public void setTlsTrustCertsFilePath(String tlsTrustCertsFilePath) {
this.tlsTrustCertsFilePath = tlsTrustCertsFilePath;
}
public boolean isTlsAllowInsecureConnection() {
return tlsAllowInsecureConnection;
}
public void setTlsAllowInsecureConnection(boolean tlsAllowInsecureConnection) {
this.tlsAllowInsecureConnection = tlsAllowInsecureConnection;
}
public boolean isTlsHostnameVerificationEnable() {
return tlsHostnameVerificationEnable;
}
public void setTlsHostnameVerificationEnable(boolean tlsHostnameVerificationEnable) {
this.tlsHostnameVerificationEnable = tlsHostnameVerificationEnable;
}
public int getConcurrentLookupRequest() {
return concurrentLookupRequest;
}
public void setConcurrentLookupRequest(int concurrentLookupRequest) {
this.concurrentLookupRequest = concurrentLookupRequest;
}
public int getMaxLookupRequest() {
return maxLookupRequest;
}
public void setMaxLookupRequest(int maxLookupRequest) {
this.maxLookupRequest = maxLookupRequest;
}
public int getMaxNumberOfRejectedRequestPerConnection() {
return maxNumberOfRejectedRequestPerConnection;
}
public void setMaxNumberOfRejectedRequestPerConnection(int maxNumberOfRejectedRequestPerConnection) {
this.maxNumberOfRejectedRequestPerConnection = maxNumberOfRejectedRequestPerConnection;
}
public int getKeepAliveIntervalSeconds() {
return keepAliveIntervalSeconds;
}
public void setKeepAliveIntervalSeconds(int keepAliveIntervalSeconds) {
this.keepAliveIntervalSeconds = keepAliveIntervalSeconds;
}
public int getConnectionTimeoutMs() {
return connectionTimeoutMs;
}
public void setConnectionTimeoutMs(int connectionTimeoutMs) {
this.connectionTimeoutMs = connectionTimeoutMs;
}
public int getRequestTimeoutMs() {
return requestTimeoutMs;
}
public void setRequestTimeoutMs(int requestTimeoutMs) {
this.requestTimeoutMs = requestTimeoutMs;
}
public long getInitialBackoffIntervalNanos() {
return initialBackoffIntervalNanos;
}
public void setInitialBackoffIntervalNanos(long initialBackoffIntervalNanos) {
this.initialBackoffIntervalNanos = initialBackoffIntervalNanos;
}
public long getMaxBackoffIntervalNanos() {
return maxBackoffIntervalNanos;
}
public void setMaxBackoffIntervalNanos(long maxBackoffIntervalNanos) {
this.maxBackoffIntervalNanos = maxBackoffIntervalNanos;
}
public Map<String, Object> buildProperties() {
PulsarProperties.Properties properties = new Properties();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this::getServiceUrl).to(properties.in("serviceUrl"));
map.from(this::getAuthPluginClassName).to(properties.in("authPluginClassName"));
map.from(this::getAuthParams).to(properties.in("authParams"));
map.from(this::getOperationTimeoutMs).to(properties.in("operationTimeoutMs"));
map.from(this::getStatsIntervalSeconds).to(properties.in("statsIntervalSeconds"));
map.from(this::getNumIoThreads).to(properties.in("numIoThreads"));
map.from(this::isUseTcpNoDelay).to(properties.in("useTcpNoDelay"));
map.from(this::isUseTls).to(properties.in("useTls"));
map.from(this::getTlsTrustCertsFilePath).to(properties.in("tlsTrustCertsFilePath"));
map.from(this::isTlsAllowInsecureConnection).to(properties.in("tlsAllowInsecureConnection"));
map.from(this::isTlsHostnameVerificationEnable).to(properties.in("tlsHostnameVerificationEnable"));
map.from(this::getConcurrentLookupRequest).to(properties.in("concurrentLookupRequest"));
map.from(this::getMaxLookupRequest).to(properties.in("maxLookupRequest"));
map.from(this::getMaxNumberOfRejectedRequestPerConnection).to(properties.in("maxNumberOfRejectedRequestPerConnection"));
map.from(this::getKeepAliveIntervalSeconds).to(properties.in("keepAliveIntervalSeconds"));
map.from(this::getConnectionTimeoutMs).to(properties.in("connectionTimeoutMs"));
map.from(this::getRequestTimeoutMs).to(properties.in("requestTimeoutMs"));
map.from(this::getInitialBackoffIntervalNanos).to(properties.in("initialBackoffIntervalNanos"));
map.from(this::getMaxBackoffIntervalNanos).to(properties.in("maxBackoffIntervalNanos"));
return properties;
}
}
public static class Listener {
private PulsarProperties.Schema schema = Schema.STRING;
public PulsarProperties.Schema getSchema() {
return schema;
}
public void setSchema(PulsarProperties.Schema schema) {
this.schema = schema;
}
}
@SuppressWarnings("serial")
private static class Properties extends HashMap<String, Object> {
<V> java.util.function.Consumer<V> in(String key) {
return (value) -> put(key, value);
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.log.LogAccessor;
import org.springframework.pulsar.listener.AbstractPulsarMessageListenerContainer;
import org.springframework.pulsar.support.JavaUtils;
import org.springframework.pulsar.support.MessageConverter;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.listener.PulsarContainerProperties;
/**
* @author Soby Chacko
*/
public abstract class AbstractPulsarListenerContainerFactory<C extends AbstractPulsarMessageListenerContainer<T>, T>
implements PulsarListenerContainerFactory<C>, ApplicationEventPublisherAware, InitializingBean,
ApplicationContextAware {
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
private final PulsarContainerProperties containerProperties = new PulsarContainerProperties();
private PulsarConsumerFactory<? super T> consumerFactory;
private Boolean autoStartup;
private Integer phase;
private MessageConverter messageConverter;
private Boolean batchListener;
private ApplicationEventPublisher applicationEventPublisher;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public void setPulsarConsumerFactory(PulsarConsumerFactory<? super T> consumerFactory) {
this.consumerFactory = consumerFactory;
}
public PulsarConsumerFactory<? super T> getPulsarConsumerFactory() {
return this.consumerFactory;
}
public void setAutoStartup(Boolean autoStartup) {
this.autoStartup = autoStartup;
}
public void setPhase(int phase) {
this.phase = phase;
}
/**
* Set the message converter to use if dynamic argument type matching is needed.
* @param messageConverter the converter.
*/
public void setMessageConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
public Boolean isBatchListener() {
return this.batchListener;
}
public void setBatchListener(Boolean batchListener) {
this.batchListener = batchListener;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public PulsarContainerProperties getContainerProperties() {
return this.containerProperties;
}
@Override
public void afterPropertiesSet() {
}
@SuppressWarnings("unchecked")
@Override
public C createListenerContainer(PulsarListenerEndpoint endpoint) {
C instance = createContainerInstance(endpoint);
JavaUtils.INSTANCE
.acceptIfNotNull(endpoint.getSubscriptionName(), instance::setBeanName);
if (endpoint instanceof AbstractPulsarListenerEndpoint) {
configureEndpoint((AbstractPulsarListenerEndpoint<C>) endpoint);
}
endpoint.setupListenerContainer(instance, this.messageConverter);
initializeContainer(instance, endpoint);
//customizeContainer(instance);
return instance;
}
protected abstract C createContainerInstance(PulsarListenerEndpoint endpoint);
private void configureEndpoint(AbstractPulsarListenerEndpoint<C> aplEndpoint) {
if (aplEndpoint.getBatchListener() == null) {
JavaUtils.INSTANCE
.acceptIfNotNull(this.batchListener, aplEndpoint::setBatchListener);
}
}
protected void initializeContainer(C instance, PulsarListenerEndpoint endpoint) {
PulsarContainerProperties properties = instance.getPulsarContainerProperties();
BeanUtils.copyProperties(this.containerProperties, properties, "topics", "messageListener", "batchReceive", "subscriptionName");
Boolean autoStart = endpoint.getAutoStartup();
if (autoStart != null) {
instance.setAutoStartup(autoStart);
}
else if (this.autoStartup != null) {
instance.setAutoStartup(this.autoStartup);
}
JavaUtils.INSTANCE
.acceptIfNotNull(this.phase, instance::setPhase)
.acceptIfNotNull(this.applicationContext, instance::setApplicationContext)
.acceptIfNotNull(this.applicationEventPublisher, instance::setApplicationEventPublisher);
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import org.apache.commons.logging.LogFactory;
import org.apache.pulsar.client.api.SubscriptionType;
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.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.support.MessageConverter;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public abstract class AbstractPulsarListenerEndpoint<K> implements PulsarListenerEndpoint, BeanFactoryAware, InitializingBean {
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
private String subscriptionName;
private SubscriptionType subscriptionType;
private String id;
private final Collection<String> topics = new ArrayList<>();
private BeanFactory beanFactory;
private BeanExpressionResolver resolver;
private BeanExpressionContext expressionContext;
private BeanResolver beanResolver;
private Boolean autoStartup;
private Properties consumerProperties;
private Boolean batchListener;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.resolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver();
this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null);
}
this.beanResolver = new BeanFactoryResolver(beanFactory);
}
@Nullable
protected BeanFactory getBeanFactory() {
return this.beanFactory;
}
@Override
public void afterPropertiesSet() {
boolean topicsEmpty = getTopics().isEmpty();
if (!topicsEmpty) {
throw new IllegalStateException("Topics or topicPartitions must be provided but not both for " + this);
}
}
@Nullable
protected BeanExpressionResolver getResolver() {
return this.resolver;
}
@Nullable
protected BeanExpressionContext getBeanExpressionContext() {
return this.expressionContext;
}
@Nullable
protected BeanResolver getBeanResolver() {
return this.beanResolver;
}
public void setSubscriptionName(String subscriptionName) {
this.subscriptionName = subscriptionName;
}
@Nullable
@Override
public String getSubscriptionName() {
return this.subscriptionName;
}
public void setId(String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
public void setTopics(String... topics) {
Assert.notNull(topics, "'topics' must not be null");
this.topics.clear();
this.topics.addAll(Arrays.asList(topics));
}
@Override
public Collection<String> getTopics() {
return Collections.unmodifiableCollection(this.topics);
}
@Override
@Nullable
public Boolean getAutoStartup() {
return this.autoStartup;
}
public void setAutoStartup(Boolean autoStartup) {
this.autoStartup = autoStartup;
}
@Override
public void setupListenerContainer(PulsarMessageListenerContainer listenerContainer,
@Nullable MessageConverter messageConverter) {
setupMessageListener(listenerContainer, messageConverter);
}
@SuppressWarnings("unchecked")
private void setupMessageListener(PulsarMessageListenerContainer container,
@Nullable MessageConverter messageConverter) {
PulsarMessagingMessageListenerAdapter<K> adapter = createMessageListener(container, messageConverter);
Object messageListener = adapter;
boolean isBatchListener = isBatchListener();
Assert.state(messageListener != null,
() -> "Endpoint [" + this + "] must provide a non null message listener");
container.setupMessageListener(messageListener);
}
protected abstract PulsarMessagingMessageListenerAdapter<K> createMessageListener(PulsarMessageListenerContainer container,
@Nullable MessageConverter messageConverter);
public void setConsumerProperties(Properties consumerProperties) {
this.consumerProperties = consumerProperties;
}
@Nullable
public Boolean getBatchListener() {
return this.batchListener;
}
public void setBatchListener(boolean batchListener) {
this.batchListener = batchListener;
}
public boolean isBatchListener() {
return this.batchListener == null ? false : this.batchListener;
}
public SubscriptionType getSubscriptionType() {
return subscriptionType;
}
public void setSubscriptionType(SubscriptionType subscriptionType) {
this.subscriptionType = subscriptionType;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.lang.reflect.Method;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.lang.Nullable;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.pulsar.listener.adapter.HandlerAdapter;
import org.springframework.pulsar.support.MessageConverter;
import org.springframework.pulsar.support.converter.PulsarBatchMessageConverter;
import org.springframework.pulsar.listener.adapter.PulsarBatchMessagingMessageListenerAdapter;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
import org.springframework.pulsar.support.converter.PulsarRecordMessageConverter;
import org.springframework.pulsar.listener.adapter.PulsarRecordMessagingMessageListenerAdapter;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public class MethodPulsarListenerEndpoint<V> extends AbstractPulsarListenerEndpoint<V> {
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
private Object bean;
private Method method;
private MessageHandlerMethodFactory messageHandlerMethodFactory;
private SmartMessageConverter messagingConverter;
public void setBean(Object bean) {
this.bean = bean;
}
public Object getBean() {
return this.bean;
}
/**
* Set the method to invoke to process a message managed by this endpoint.
*
* @param method the target method for the {@link #bean}.
*/
public void setMethod(Method method) {
this.method = method;
}
public Method getMethod() {
return this.method;
}
public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory messageHandlerMethodFactory) {
this.messageHandlerMethodFactory = messageHandlerMethodFactory;
}
@Override
protected PulsarMessagingMessageListenerAdapter<V> createMessageListener(PulsarMessageListenerContainer container,
@Nullable MessageConverter messageConverter) {
Assert.state(this.messageHandlerMethodFactory != null,
"Could not create message listener - MessageHandlerMethodFactory not set");
PulsarMessagingMessageListenerAdapter<V> messageListener = createMessageListenerInstance(messageConverter);
messageListener.setHandlerMethod(configureListenerAdapter(messageListener));
return messageListener;
}
protected HandlerAdapter configureListenerAdapter(PulsarMessagingMessageListenerAdapter<V> messageListener) {
InvocableHandlerMethod invocableHandlerMethod =
this.messageHandlerMethodFactory.createInvocableHandlerMethod(getBean(), getMethod());
return new HandlerAdapter(invocableHandlerMethod);
}
protected PulsarMessagingMessageListenerAdapter<V> createMessageListenerInstance(
@Nullable MessageConverter messageConverter) {
PulsarMessagingMessageListenerAdapter<V> listener;
if (isBatchListener()) {
PulsarBatchMessagingMessageListenerAdapter<V> messageListener = new PulsarBatchMessagingMessageListenerAdapter<V>(
this.bean, this.method);
if (messageConverter instanceof PulsarBatchMessageConverter) {
messageListener.setBatchMessageConverter((PulsarBatchMessageConverter) messageConverter);
}
listener = messageListener;
}
else {
PulsarRecordMessagingMessageListenerAdapter<V> messageListener = new PulsarRecordMessagingMessageListenerAdapter<V>(
this.bean, this.method);
if (messageConverter instanceof PulsarRecordMessageConverter) {
messageListener.setMessageConverter((PulsarRecordMessageConverter) messageConverter);
}
listener = messageListener;
}
if (this.messagingConverter != null) {
listener.setMessagingConverter(this.messagingConverter);
}
BeanResolver resolver = getBeanResolver();
if (resolver != null) {
listener.setBeanResolver(resolver);
}
return listener;
}
public void setMessagingConverter(SmartMessageConverter messagingConverter) {
this.messagingConverter = messagingConverter;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public class PulsarClientConfiguration {
private final Map<String, Object> configs = new HashMap<>();
public PulsarClientConfiguration() {
this(Map.of("serviceUrl", "pulsar://localhost:6650"));
}
public PulsarClientConfiguration(Map<String, Object> configs) {
Assert.notNull(configs, "Configuration map cannot be null");
this.configs.putAll(configs);
this.configs.putIfAbsent("serviceUrl", "pulsar://localhost:6650");
}
public Map<String, Object> getConfigs() {
return configs;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import org.apache.pulsar.client.api.PulsarClient;
import org.springframework.beans.factory.config.AbstractFactoryBean;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.config.PulsarClientConfiguration;
/**
* @author Soby Chacko
*/
public class PulsarClientFactoryBean extends AbstractFactoryBean<PulsarClient> {
private final PulsarClientConfiguration pulsarClientConfiguration;
public PulsarClientFactoryBean(PulsarClientConfiguration pulsarClientConfiguration) {
this.pulsarClientConfiguration = pulsarClientConfiguration;
}
@Override
public Class<?> getObjectType() {
return PulsarClient.class;
}
@Override
protected PulsarClient createInstance() throws Exception {
return PulsarClient.builder()
.loadConf(this.pulsarClientConfiguration.getConfigs())
.build();
}
@Override
protected void destroyInstance(PulsarClient instance) throws Exception {
if (instance != null) {
System.out.printf("CLOSING THE CLIENT");
instance.close();
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
/**
* @author Soby Chacko
*/
public abstract class PulsarListenerConfigUtils {
/**
* The bean name of the internally managed Kafka listener annotation processor.
*/
public static final String PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME =
"org.springframework.pulsar.config.internalKafkaListenerAnnotationProcessor";
/**
* The bean name of the internally managed Kafka listener endpoint registry.
*/
public static final String PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME =
"org.springframework.pulsar.config.internalKafkaListenerEndpointRegistry";
/**
* The bean name of the internally managed Kafka consumer back off manager.
*/
public static final String PULSAR_CONSUMER_BACK_OFF_MANAGER_BEAN_NAME =
"org.springframework.pulsar.config.internalKafkaConsumerBackOffManager";
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
/**
* @author Soby Chacko
*/
public interface PulsarListenerContainerFactory<C extends PulsarMessageListenerContainer> {
C createListenerContainer(PulsarListenerEndpoint endpoint);
C createContainer(String... topics);
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.Arrays;
import java.util.Collection;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.pulsar.listener.DefaultPulsarMessageListenerContainer;
import org.springframework.pulsar.listener.PulsarContainerProperties;
import org.springframework.util.StringUtils;
/**
* @author Soby Chacko
*/
public class PulsarListenerContainerFactoryImpl<C, T> extends AbstractPulsarListenerContainerFactory<DefaultPulsarMessageListenerContainer<T>, T> {
@Override
protected DefaultPulsarMessageListenerContainer<T> createContainerInstance(PulsarListenerEndpoint endpoint) {
PulsarContainerProperties properties = new PulsarContainerProperties();
Collection<String> topics = endpoint.getTopics();
if (!topics.isEmpty()) {
final String[] topics1 = topics.toArray(new String[0]);
properties.setTopics(topics1);
}
final String subscriptionName = endpoint.getSubscriptionName();
if (StringUtils.hasText(subscriptionName)) {
properties.setSubscriptionName(endpoint.getSubscriptionName());
}
if (endpoint.isBatchListener()) {
properties.setBatchReceive(endpoint.isBatchListener());
}
final SubscriptionType subscriptionType = endpoint.getSubscriptionType();
if (subscriptionType != null) {
properties.setSubscriptionType(subscriptionType);
}
return new DefaultPulsarMessageListenerContainer<T>(getPulsarConsumerFactory(), properties);
}
@Override
protected void initializeContainer(DefaultPulsarMessageListenerContainer<T> instance,
PulsarListenerEndpoint endpoint) {
super.initializeContainer(instance, endpoint);
}
@Override
public DefaultPulsarMessageListenerContainer<T> createContainer(String... topics) {
PulsarListenerEndpoint endpoint = new PulsarListenerEndpointAdapter() {
@Override
public Collection<String> getTopics() {
return Arrays.asList(topics);
}
};
DefaultPulsarMessageListenerContainer<T> container = createContainerInstance(endpoint);
initializeContainer(container, endpoint);
//customizeContainer(container);
return container;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.Collection;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.support.MessageConverter;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
/**
* @author Soby Chacko
*/
public interface PulsarListenerEndpoint {
@Nullable
String getId();
@Nullable
String getSubscriptionName();
@Nullable
SubscriptionType getSubscriptionType();
Collection<String> getTopics();
@Nullable
Boolean getAutoStartup();
void setupListenerContainer(PulsarMessageListenerContainer listenerContainer,
@Nullable MessageConverter messageConverter);
boolean isBatchListener();
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.Collection;
import java.util.Collections;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.pulsar.support.MessageConverter;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
/**
* @author Soby Chacko
*/
public class PulsarListenerEndpointAdapter implements PulsarListenerEndpoint {
@Override
public String getId() {
return null;
}
@Override
public String getSubscriptionName() {
return null;
}
@Override
public SubscriptionType getSubscriptionType() {
return SubscriptionType.Exclusive;
}
@Override
public Collection<String> getTopics() {
return Collections.emptyList();
}
@Override
public Boolean getAutoStartup() {
return null;
}
@Override
public void setupListenerContainer(PulsarMessageListenerContainer listenerContainer, MessageConverter messageConverter) {
}
@Override
public boolean isBatchListener() {
return false;
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.Nullable;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.util.Assert;
import org.springframework.validation.Validator;
/**
* @author Soby Chacko
*/
public class PulsarListenerEndpointRegistrar implements BeanFactoryAware, InitializingBean {
private final List<PulsarListenerEndpointDescriptor> endpointDescriptors = new ArrayList<>();
private PulsarListenerEndpointRegistry endpointRegistry;
private List<HandlerMethodArgumentResolver> customMethodArgumentResolvers = new ArrayList<>();
private Validator validator;
private MessageHandlerMethodFactory messageHandlerMethodFactory;
private PulsarListenerContainerFactory<?> containerFactory;
private String containerFactoryBeanName;
private BeanFactory beanFactory;
private boolean startImmediately;
public void setEndpointRegistry(PulsarListenerEndpointRegistry endpointRegistry) {
this.endpointRegistry = endpointRegistry;
}
@Nullable
public PulsarListenerEndpointRegistry getEndpointRegistry() {
return this.endpointRegistry;
}
public List<HandlerMethodArgumentResolver> getCustomMethodArgumentResolvers() {
return Collections.unmodifiableList(this.customMethodArgumentResolvers);
}
public void setCustomMethodArgumentResolvers(HandlerMethodArgumentResolver... methodArgumentResolvers) {
this.customMethodArgumentResolvers = Arrays.asList(methodArgumentResolvers);
}
public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory PulsarHandlerMethodFactory) {
Assert.isNull(this.validator,
"A validator cannot be provided with a custom message handler factory");
this.messageHandlerMethodFactory = PulsarHandlerMethodFactory;
}
@Nullable
public MessageHandlerMethodFactory getMessageHandlerMethodFactory() {
return this.messageHandlerMethodFactory;
}
public void setContainerFactory(PulsarListenerContainerFactory<?> containerFactory) {
this.containerFactory = containerFactory;
}
public void setContainerFactoryBeanName(String containerFactoryBeanName) {
this.containerFactoryBeanName = containerFactoryBeanName;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Nullable
public Validator getValidator() {
return this.validator;
}
public void setValidator(Validator validator) {
Assert.isNull(this.messageHandlerMethodFactory,
"A validator cannot be provided with a custom message handler factory");
this.validator = validator;
}
@Override
public void afterPropertiesSet() {
registerAllEndpoints();
}
protected void registerAllEndpoints() {
synchronized (this.endpointDescriptors) {
for (PulsarListenerEndpointDescriptor descriptor : this.endpointDescriptors) {
this.endpointRegistry.registerListenerContainer(
descriptor.endpoint, resolveContainerFactory(descriptor));
}
this.startImmediately = true; // trigger immediate startup
}
}
private PulsarListenerContainerFactory<?> resolveContainerFactory(PulsarListenerEndpointDescriptor descriptor) {
if (descriptor.containerFactory != null) {
return descriptor.containerFactory;
}
else if (this.containerFactory != null) {
return this.containerFactory;
}
else if (this.containerFactoryBeanName != null) {
Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");
this.containerFactory = this.beanFactory.getBean(
this.containerFactoryBeanName, PulsarListenerContainerFactory.class);
return this.containerFactory; // Consider changing this if live change of the factory is required
}
else {
throw new IllegalStateException("Could not resolve the " +
PulsarListenerContainerFactory.class.getSimpleName() + " to use for [" +
descriptor.endpoint + "] no factory was given and no default is set.");
}
}
public void registerEndpoint(PulsarListenerEndpoint endpoint, @Nullable PulsarListenerContainerFactory<?> factory) {
Assert.notNull(endpoint, "Endpoint must be set");
Assert.hasText(endpoint.getSubscriptionName(), "Endpoint id must be set");
// Factory may be null, we defer the resolution right before actually creating the container
PulsarListenerEndpointDescriptor descriptor = new PulsarListenerEndpointDescriptor(endpoint, factory);
synchronized (this.endpointDescriptors) {
if (this.startImmediately) { // Register and start immediately
this.endpointRegistry.registerListenerContainer(descriptor.endpoint,
resolveContainerFactory(descriptor), true);
}
else {
this.endpointDescriptors.add(descriptor);
}
}
}
private static final class PulsarListenerEndpointDescriptor {
private final PulsarListenerEndpoint endpoint;
private final PulsarListenerContainerFactory<?> containerFactory;
private PulsarListenerEndpointDescriptor(PulsarListenerEndpoint endpoint,
@Nullable PulsarListenerContainerFactory<?> containerFactory) {
this.endpoint = endpoint;
this.containerFactory = containerFactory;
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.listener.AbstractPulsarMessageListenerContainer;
import org.springframework.pulsar.listener.PulsarListenerContainerRegistry;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
import org.springframework.pulsar.support.EndpointHandlerMethod;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public class PulsarListenerEndpointRegistry implements PulsarListenerContainerRegistry, DisposableBean, SmartLifecycle,
ApplicationContextAware, ApplicationListener<ContextRefreshedEvent> {
private final Map<String, PulsarMessageListenerContainer> listenerContainers = new ConcurrentHashMap<>();
private ConfigurableApplicationContext applicationContext;
private int phase = AbstractPulsarMessageListenerContainer.DEFAULT_PHASE;
private boolean contextRefreshed;
private volatile boolean running;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (applicationContext instanceof ConfigurableApplicationContext) {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
}
}
@Override
@Nullable
public PulsarMessageListenerContainer getListenerContainer(String id) {
Assert.hasText(id, "Container identifier must not be empty");
return this.listenerContainers.get(id);
}
@Override
public Set<String> getListenerContainerIds() {
return Collections.unmodifiableSet(this.listenerContainers.keySet());
}
@Override
public Collection<PulsarMessageListenerContainer> getListenerContainers() {
return Collections.unmodifiableCollection(this.listenerContainers.values());
}
@Override
public Collection<PulsarMessageListenerContainer> getAllListenerContainers() {
List<PulsarMessageListenerContainer> containers = new ArrayList<>();
containers.addAll(getListenerContainers());
containers.addAll(this.applicationContext.getBeansOfType(PulsarMessageListenerContainer.class, true, false).values());
return containers;
}
public void registerListenerContainer(PulsarListenerEndpoint endpoint, PulsarListenerContainerFactory<?> factory) {
registerListenerContainer(endpoint, factory, false);
}
public void registerListenerContainer(PulsarListenerEndpoint endpoint, PulsarListenerContainerFactory<?> factory,
boolean startImmediately) {
Assert.notNull(endpoint, "Endpoint must not be null");
Assert.notNull(factory, "Factory must not be null");
String subscriptionName = endpoint.getSubscriptionName();
String id = endpoint.getId();
Assert.hasText(subscriptionName, "Endpoint id must not be empty");
synchronized (this.listenerContainers) {
Assert.state(!this.listenerContainers.containsKey(id),
"Another endpoint is already registered with id '" + subscriptionName + "'");
PulsarMessageListenerContainer container = createListenerContainer(endpoint, factory);
this.listenerContainers.put(id, container);
ConfigurableApplicationContext appContext = this.applicationContext;
}
}
protected PulsarMessageListenerContainer createListenerContainer(PulsarListenerEndpoint endpoint,
PulsarListenerContainerFactory<?> factory) {
if (endpoint instanceof MethodPulsarListenerEndpoint) {
MethodPulsarListenerEndpoint<?> mkle = (MethodPulsarListenerEndpoint<?>) endpoint;
Object bean = mkle.getBean();
if (bean instanceof EndpointHandlerMethod) {
EndpointHandlerMethod ehm = (EndpointHandlerMethod) bean;
ehm = new EndpointHandlerMethod(ehm.resolveBean(this.applicationContext), ehm.getMethodName());
mkle.setBean(ehm.resolveBean(this.applicationContext));
mkle.setMethod(ehm.getMethod());
}
}
PulsarMessageListenerContainer listenerContainer = factory.createListenerContainer(endpoint);
if (listenerContainer instanceof InitializingBean) {
try {
((InitializingBean) listenerContainer).afterPropertiesSet();
}
catch (Exception ex) {
throw new BeanInitializationException("Failed to initialize message listener container", ex);
}
}
int containerPhase = listenerContainer.getPhase();
if (listenerContainer.isAutoStartup() &&
containerPhase != AbstractPulsarMessageListenerContainer.DEFAULT_PHASE) { // a custom phase value
if (this.phase != AbstractPulsarMessageListenerContainer.DEFAULT_PHASE && this.phase != containerPhase) {
throw new IllegalStateException("Encountered phase mismatch between container "
+ "factory definitions: " + this.phase + " vs " + containerPhase);
}
this.phase = listenerContainer.getPhase();
}
return listenerContainer;
}
@Override
public void destroy() {
for (PulsarMessageListenerContainer listenerContainer : getListenerContainers()) {
listenerContainer.destroy();
}
}
// Delegating implementation of SmartLifecycle
@Override
public int getPhase() {
return this.phase;
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public void start() {
for (PulsarMessageListenerContainer listenerContainer : getListenerContainers()) {
startIfNecessary(listenerContainer);
}
this.running = true;
}
@Override
public void stop() {
this.running = false;
for (PulsarMessageListenerContainer listenerContainer : getListenerContainers()) {
listenerContainer.stop();
}
}
@Override
public void stop(Runnable callback) {
this.running = false;
Collection<PulsarMessageListenerContainer> listenerContainersToStop = getListenerContainers();
if (listenerContainersToStop.size() > 0) {
AggregatingCallback aggregatingCallback = new AggregatingCallback(listenerContainersToStop.size(),
callback);
for (PulsarMessageListenerContainer listenerContainer : listenerContainersToStop) {
if (listenerContainer.isRunning()) {
listenerContainer.stop(aggregatingCallback);
}
else {
aggregatingCallback.run();
}
}
}
else {
callback.run();
}
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().equals(this.applicationContext)) {
this.contextRefreshed = true;
}
}
private void startIfNecessary(PulsarMessageListenerContainer listenerContainer) {
if (this.contextRefreshed || listenerContainer.isAutoStartup()) {
listenerContainer.start();
}
}
private static final class AggregatingCallback implements Runnable {
private final AtomicInteger count;
private final Runnable finishCallback;
private AggregatingCallback(int count, Runnable finishCallback) {
this.count = new AtomicInteger(count);
this.finishCallback = finishCallback;
}
@Override
public void run() {
if (this.count.decrementAndGet() <= 0) {
this.finishCallback.run();
}
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.pulsar.client.api.BatchReceivePolicy;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.ConsumerBuilder;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
import org.springframework.util.CollectionUtils;
/**
* @author Soby Chacko
*/
public class DefaultPulsarConsumerFactory<T> implements PulsarConsumerFactory<T> {
private final Map<String, Object> consumerConfig = new HashMap<>();
private final List<Consumer<T>> consumers = new ArrayList<>();
private PulsarClient pulsarClient;
public DefaultPulsarConsumerFactory(PulsarClient pulsarClient, Map<String, Object> consumerConfig) {
this.pulsarClient = pulsarClient;
if (!CollectionUtils.isEmpty(consumerConfig)) {
this.consumerConfig.putAll(consumerConfig);
}
}
@Override
public Consumer<T> createConsumer(Schema<T> schema, Map<String, Object> propertiesToOverride) throws PulsarClientException {
final ConsumerBuilder<T> consumerBuilder = this.pulsarClient.newConsumer(schema);
final Map<String, Object> properties = new HashMap<>(this.consumerConfig);
properties.putAll(propertiesToOverride);
if (!CollectionUtils.isEmpty(properties)) {
consumerBuilder.loadConf(properties);
}
Consumer<T> consumer = consumerBuilder.subscribe();
consumers.add(consumer);
return consumer;
}
@Override
public Consumer<T> createConsumer(Schema<T> schema, BatchReceivePolicy batchReceivePolicy, Map<String, Object> propertiesToOverride) throws PulsarClientException {
final ConsumerBuilder<T> consumerBuilder = this.pulsarClient.newConsumer(schema);
final Map<String, Object> properties = new HashMap<>(this.consumerConfig);
properties.putAll(propertiesToOverride);
if (!CollectionUtils.isEmpty(properties)) {
consumerBuilder.loadConf(properties);
}
consumerBuilder.batchReceivePolicy(batchReceivePolicy);
Consumer<T> consumer = consumerBuilder.subscribe();
consumers.add(consumer);
return consumer;
}
public Map<String, Object> getConsumerConfig() {
return consumerConfig;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.LogFactory;
import org.apache.pulsar.client.api.MessageRouter;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.ProducerBuilder;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.log.LogAccessor;
import org.springframework.util.CollectionUtils;
/**
* @author Soby Chacko
*/
public class DefaultPulsarProducerFactory<T> implements PulsarProducerFactory<T>, DisposableBean {
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(this.getClass()));
private final Map<String, Object> producerConfig = new HashMap<>();
private Producer<T> producer;
private final PulsarClient pulsarClient;
public DefaultPulsarProducerFactory(PulsarClient pulsarClient, Map<String, Object> config) {
this.pulsarClient = pulsarClient;
if (!CollectionUtils.isEmpty(config)) {
this.producerConfig.putAll(config);
}
}
@Override
public Producer<T> createProducer(Schema<T> schema) throws PulsarClientException {
final ProducerBuilder<T> producerBuilder = this.pulsarClient.newProducer(schema);
if (!CollectionUtils.isEmpty(this.producerConfig)) {
producerBuilder.loadConf(this.producerConfig);
}
this.producer = producerBuilder.create();
return producer;
}
@Override
public Producer<T> createProducer(Schema<T> schema, MessageRouter messageRouter) throws PulsarClientException {
final ProducerBuilder<T> producerBuilder = this.pulsarClient.newProducer(schema);
if (!CollectionUtils.isEmpty(this.producerConfig)) {
producerBuilder.loadConf(this.producerConfig);
}
producerBuilder.messageRouter(messageRouter);
this.producer = producerBuilder.create();
return producer;
}
@Override
public Map<String, Object> getProducerConfig() {
return producerConfig;
}
@Override
public void destroy() throws Exception {
this.logger.info("Closing the producer");
this.producer.close();
}
}

View File

@@ -0,0 +1,245 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.net.SocketAddress;
/**
* @author Soby Chacko
*/
public class PulsarClientProperties {
private String serviceUrl;
private String authPluginClassName;
private String authParams;
private long operationTimeoutMs;
private long statsIntervalSeconds;
private int numIoThreads;
private boolean useTcpNoDelay;
private boolean useTls;
private String tlsTrustCertsFilePath;
private boolean tlsAllowInsecureConnection;
private boolean tlsHostnameVerificationEnable;
private int concurrentLookupRequest;
private int maxLookupRequest;
private int maxNumberOfRejectedRequestPerConnection;
private int keepAliveIntervalSeconds;
private int connectionTimeoutMs;
private int requestTimeoutMs;
private int defaultBackoffIntervalNanos;
private long maxBackoffIntervalNanos;
private SocketAddress socks5ProxyAddress;
private String socks5ProxyUsername;
private String socks5ProxyPassword;
public String getServiceUrl() {
return serviceUrl;
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
public String getAuthPluginClassName() {
return authPluginClassName;
}
public void setAuthPluginClassName(String authPluginClassName) {
this.authPluginClassName = authPluginClassName;
}
public String getAuthParams() {
return authParams;
}
public void setAuthParams(String authParams) {
this.authParams = authParams;
}
public long getOperationTimeoutMs() {
return operationTimeoutMs;
}
public void setOperationTimeoutMs(long operationTimeoutMs) {
this.operationTimeoutMs = operationTimeoutMs;
}
public long getStatsIntervalSeconds() {
return statsIntervalSeconds;
}
public void setStatsIntervalSeconds(long statsIntervalSeconds) {
this.statsIntervalSeconds = statsIntervalSeconds;
}
public int getNumIoThreads() {
return numIoThreads;
}
public void setNumIoThreads(int numIoThreads) {
this.numIoThreads = numIoThreads;
}
public boolean isUseTcpNoDelay() {
return useTcpNoDelay;
}
public void setUseTcpNoDelay(boolean useTcpNoDelay) {
this.useTcpNoDelay = useTcpNoDelay;
}
public boolean isUseTls() {
return useTls;
}
public void setUseTls(boolean useTls) {
this.useTls = useTls;
}
public String getTlsTrustCertsFilePath() {
return tlsTrustCertsFilePath;
}
public void setTlsTrustCertsFilePath(String tlsTrustCertsFilePath) {
this.tlsTrustCertsFilePath = tlsTrustCertsFilePath;
}
public boolean isTlsAllowInsecureConnection() {
return tlsAllowInsecureConnection;
}
public void setTlsAllowInsecureConnection(boolean tlsAllowInsecureConnection) {
this.tlsAllowInsecureConnection = tlsAllowInsecureConnection;
}
public boolean isTlsHostnameVerificationEnable() {
return tlsHostnameVerificationEnable;
}
public void setTlsHostnameVerificationEnable(boolean tlsHostnameVerificationEnable) {
this.tlsHostnameVerificationEnable = tlsHostnameVerificationEnable;
}
public int getConcurrentLookupRequest() {
return concurrentLookupRequest;
}
public void setConcurrentLookupRequest(int concurrentLookupRequest) {
this.concurrentLookupRequest = concurrentLookupRequest;
}
public int getMaxLookupRequest() {
return maxLookupRequest;
}
public void setMaxLookupRequest(int maxLookupRequest) {
this.maxLookupRequest = maxLookupRequest;
}
public int getMaxNumberOfRejectedRequestPerConnection() {
return maxNumberOfRejectedRequestPerConnection;
}
public void setMaxNumberOfRejectedRequestPerConnection(int maxNumberOfRejectedRequestPerConnection) {
this.maxNumberOfRejectedRequestPerConnection = maxNumberOfRejectedRequestPerConnection;
}
public int getKeepAliveIntervalSeconds() {
return keepAliveIntervalSeconds;
}
public void setKeepAliveIntervalSeconds(int keepAliveIntervalSeconds) {
this.keepAliveIntervalSeconds = keepAliveIntervalSeconds;
}
public int getConnectionTimeoutMs() {
return connectionTimeoutMs;
}
public void setConnectionTimeoutMs(int connectionTimeoutMs) {
this.connectionTimeoutMs = connectionTimeoutMs;
}
public int getRequestTimeoutMs() {
return requestTimeoutMs;
}
public void setRequestTimeoutMs(int requestTimeoutMs) {
this.requestTimeoutMs = requestTimeoutMs;
}
public int getDefaultBackoffIntervalNanos() {
return defaultBackoffIntervalNanos;
}
public void setDefaultBackoffIntervalNanos(int defaultBackoffIntervalNanos) {
this.defaultBackoffIntervalNanos = defaultBackoffIntervalNanos;
}
public long getMaxBackoffIntervalNanos() {
return maxBackoffIntervalNanos;
}
public void setMaxBackoffIntervalNanos(long maxBackoffIntervalNanos) {
this.maxBackoffIntervalNanos = maxBackoffIntervalNanos;
}
public SocketAddress getSocks5ProxyAddress() {
return socks5ProxyAddress;
}
public void setSocks5ProxyAddress(SocketAddress socks5ProxyAddress) {
this.socks5ProxyAddress = socks5ProxyAddress;
}
public String getSocks5ProxyUsername() {
return socks5ProxyUsername;
}
public void setSocks5ProxyUsername(String socks5ProxyUsername) {
this.socks5ProxyUsername = socks5ProxyUsername;
}
public String getSocks5ProxyPassword() {
return socks5ProxyPassword;
}
public void setSocks5ProxyPassword(String socks5ProxyPassword) {
this.socks5ProxyPassword = socks5ProxyPassword;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.Map;
import org.apache.pulsar.client.api.BatchReceivePolicy;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
/**
* @author Soby Chacko
*/
public interface PulsarConsumerFactory<T> {
Consumer<T> createConsumer(Schema<T> schema, Map<String, Object> propertiesToOverride) throws PulsarClientException;
Consumer<T> createConsumer(Schema<T> schema, BatchReceivePolicy batchReceivePolicy, Map<String, Object> propertiesToOverride) throws PulsarClientException;
Map<String, Object> getConsumerConfig();
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.Map;
import org.apache.pulsar.client.api.MessageRouter;
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;
/**
* @author Soby Chacko
*/
public interface PulsarProducerFactory<T> {
Producer<T> createProducer(Schema<T> schema) throws PulsarClientException;
Producer<T> createProducer(Schema<T> schema, MessageRouter messageRouter) throws PulsarClientException;
Map<String, Object> getProducerConfig();
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.MessageRouter;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
/**
* @author Soby Chacko
*/
public class PulsarTemplate<T> {
private final Map<SchemaTopic, Producer<T>> producerCache = new ConcurrentHashMap<>();
private final PulsarProducerFactory<T> pulsarProducerFactory;
private String defaultTopicName;
public PulsarTemplate(PulsarProducerFactory<T> pulsarProducerFactory) {
this.pulsarProducerFactory = pulsarProducerFactory;
}
public MessageId send(T message) throws PulsarClientException {
final Schema<T> schema = SchemaUtils.getSchema(message);
final SchemaTopic schemaTopic = getSchemaTopic(schema, this.pulsarProducerFactory);
Producer<T> producer = producerCache.get(schemaTopic);
if (producer == null) {
producer = this.pulsarProducerFactory.createProducer(schema);
producerCache.put(schemaTopic, producer);
}
return producer.send(message);
}
public CompletableFuture<MessageId> sendAsync(T message) throws PulsarClientException {
final Schema<T> schema = SchemaUtils.getSchema(message);
final SchemaTopic schemaTopic = getSchemaTopic(schema, this.pulsarProducerFactory);
Producer<T> producer = producerCache.get(schemaTopic);
if (producer == null) {
producer = this.pulsarProducerFactory.createProducer(schema);
producerCache.put(schemaTopic, producer);
}
return producer.sendAsync(message);
}
public CompletableFuture<MessageId> sendAsync(T message, MessageRouter messageRouter) throws PulsarClientException {
final Schema<T> schema = SchemaUtils.getSchema(message);
final SchemaTopic schemaTopic = getSchemaTopic(schema, this.pulsarProducerFactory);
Producer<T> producer = producerCache.get(schemaTopic);
if (producer == null) {
producer = this.pulsarProducerFactory.createProducer(schema, messageRouter);
producerCache.put(schemaTopic, producer);
}
return producer.sendAsync(message);
}
private SchemaTopic getSchemaTopic(Schema<T> schema, PulsarProducerFactory<T> pulsarProducerFactory) {
return new SchemaTopic(schema, (String) pulsarProducerFactory.getProducerConfig().get("topicName"));
}
public void setDefaultTopicName(String defaultTopicName) {
this.defaultTopicName = defaultTopicName;
this.pulsarProducerFactory.getProducerConfig().put("topicName", defaultTopicName);
}
private class SchemaTopic {
final Schema<T> schema;
final String topicName;
public SchemaTopic(Schema<T> schema, String topicName) {
this.schema = schema;
this.topicName = topicName;
}
public Schema<T> getSchema() {
return schema;
}
public String getTopicName() {
return topicName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SchemaTopic that = (SchemaTopic) o;
return Objects.equals(schema, that.schema) && Objects.equals(topicName, that.topicName);
}
@Override
public int hashCode() {
return Objects.hash(schema, topicName);
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import org.apache.pulsar.client.api.Schema;
/**
* @author Soby Chacko
*/
public class SchemaUtils {
@SuppressWarnings("unchecked")
public static <T> Schema<T> getSchema(T message) {
if (message.getClass() == byte[].class) {
return (Schema<T>) Schema.BYTES;
}
else if (message.getClass() == String.class) {
return (Schema<T>) Schema.STRING;
}
return (Schema<T>) Schema.BYTES;
}
}

View File

@@ -0,0 +1,68 @@
package org.springframework.pulsar.core;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.pulsar.config.PulsarClientConfiguration;
import org.springframework.pulsar.config.PulsarClientFactoryBean;
public class Sender {
public static void main(String[] args) throws PulsarClientException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
context.getBean(Sender.class).send();
System.exit(0);
}
private final PulsarTemplate<byte[]> template;
public Sender(PulsarTemplate<byte[]> template) {
this.template = template;
}
public void send() throws PulsarClientException {
this.template.send("foobar-fuzzy".getBytes(StandardCharsets.UTF_8));
}
}
@Configuration
class Config {
@Bean
public PulsarProducerFactory<byte[]> pulsarProducerFactory(PulsarClient pulsarClient) {
Map<String, Object> config = new HashMap<>();
config.put("topicName", "foo-1");
return new DefaultPulsarProducerFactory<>(pulsarClient, config);
}
@Bean
public PulsarTemplate<byte[]> pulsarTemplate(PulsarProducerFactory<byte[]> pulsarProducerFactory) {
return new PulsarTemplate<>(pulsarProducerFactory);
}
@Bean
public Sender sender(PulsarTemplate<byte[]> template) {
return new Sender(template);
}
@Bean
public PulsarClientFactoryBean pulsarClientFactoryBean(PulsarClientConfiguration pulsarClientConfiguration) {
return new PulsarClientFactoryBean(pulsarClientConfiguration);
}
@Bean
public PulsarClientConfiguration pulsarClientConfiguration() {
return new PulsarClientConfiguration();
}
}

View File

@@ -0,0 +1,80 @@
package org.springframework.pulsar.core;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.pulsar.config.PulsarClientConfiguration;
import org.springframework.pulsar.config.PulsarClientFactoryBean;
public class SenderString {
public static void main(String[] args) throws PulsarClientException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigString.class);
context.getBean(SenderString.class).send();
System.exit(0);
}
private final PulsarTemplate<String> template;
public SenderString(PulsarTemplate<String> template) {
this.template = template;
}
public void send() throws PulsarClientException {
final CompletableFuture<MessageId> future = this.template.sendAsync("hello john doe");
future.thenAccept(m -> System.out.println("Got " + m));
try {
future.get();
}
catch (InterruptedException e) {
e.printStackTrace();
}
catch (ExecutionException e) {
e.printStackTrace();
}
}
}
@Configuration
class ConfigString {
@Bean
public PulsarProducerFactory<String> pulsarProducerFactory(PulsarClient pulsarClient) {
Map<String, Object> config = new HashMap<>();
config.put("topicName", "foo-1");
return new DefaultPulsarProducerFactory<>(pulsarClient, config);
}
@Bean
public PulsarClientFactoryBean pulsarClientFactoryBean(PulsarClientConfiguration pulsarClientConfiguration) {
return new PulsarClientFactoryBean(pulsarClientConfiguration);
}
@Bean
public PulsarClientConfiguration pulsarClientConfiguration() {
return new PulsarClientConfiguration();
}
@Bean
public PulsarTemplate<String> pulsarTemplate(PulsarProducerFactory<String> pulsarProducerFactory) {
return new PulsarTemplate<>(pulsarProducerFactory);
}
@Bean
public SenderString senderString(PulsarTemplate<String> template) {
return new SenderString(template);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.event;
/**
* @author Soby Chacko
*/
public class ConsumerFailedToStartEvent extends PulsarEvent {
private static final long serialVersionUID = 1L;
/**
* Construct an instance with the provided source and container.
* @param source the container instance that generated the event.
* @param container the container or the parent container if the container is a child.
*/
public ConsumerFailedToStartEvent(Object source, Object container) {
super(source, container);
}
@Override
public String toString() {
return "ConsumerFailedToStartEvent [source=" + getSource() + "]";
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.event;
/**
* @author Soby Chacko
*/
public class ConsumerStartedEvent extends PulsarEvent {
private static final long serialVersionUID = 1L;
/**
* Construct an instance with the provided source and container.
* @param source the container instance that generated the event.
* @param container the container or the parent container if the container is a child.
*/
public ConsumerStartedEvent(Object source, Object container) {
super(source, container);
}
@Override
public String toString() {
return "ConsumerStartedEvent [source=" + getSource() + "]";
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.event;
/**
* @author Soby Chacko
*/
public class ConsumerStartingEvent extends PulsarEvent {
private static final long serialVersionUID = 1L;
/**
* Construct an instance with the provided source and container.
* @param source the container instance that generated the event.
* @param container the container or the parent container if the container is a child.
*/
public ConsumerStartingEvent(Object source, Object container) {
super(source, container);
}
@Override
public String toString() {
return "ConsumerStartingEvent [source=" + getSource() + "]";
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public class PulsarEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final Object container;
public PulsarEvent(Object source, Object container) {
super(source);
this.container = container;
}
@SuppressWarnings("unchecked")
public <T> T getContainer(Class<T> type) {
Assert.isInstanceOf(type, this.container);
return (T) this.container;
}
@SuppressWarnings("unchecked")
public <T> T getSource(Class<T> type) {
Assert.isInstanceOf(type, getSource());
return (T) getSource();
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.log.LogAccessor;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.core.PulsarConsumerFactory;
/**
* @author Soby Chacko
*/
public abstract class AbstractPulsarMessageListenerContainer<T>
implements PulsarMessageListenerContainer, BeanNameAware, ApplicationEventPublisherAware,
ApplicationContextAware {
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(this.getClass())); // NOSONAR
private ApplicationEventPublisher applicationEventPublisher;
private String beanName;
private ApplicationContext applicationContext;
private final PulsarContainerProperties pulsarContainerProperties;
private final PulsarConsumerFactory<T> pulsarConsumerFactory;
private boolean autoStartup = true;
private int phase;
@SuppressWarnings("unchecked")
protected AbstractPulsarMessageListenerContainer(PulsarConsumerFactory<? super T> pulsarConsumerFactory,PulsarContainerProperties pulsarContainerProperties) {
this.pulsarContainerProperties = pulsarContainerProperties;
this.pulsarConsumerFactory = (PulsarConsumerFactory<T>)pulsarConsumerFactory;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Get the event publisher.
* @return the publisher
*/
@Nullable
public ApplicationEventPublisher getApplicationEventPublisher() {
return this.applicationEventPublisher;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
/**
* Return the bean name.
* @return the bean name.
*/
@Nullable
public String getBeanName() {
return this.beanName;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Nullable
protected ApplicationContext getApplicationContext() {
return this.applicationContext;
}
@Override
public void setupMessageListener(Object messageListener) {
this.pulsarContainerProperties.setMessageListener(messageListener);
}
public PulsarContainerProperties getPulsarContainerProperties() {
return pulsarContainerProperties;
}
public PulsarConsumerFactory<? super T> getPulsarConsumerFactory() {
return pulsarConsumerFactory;
}
@Override
public boolean isAutoStartup() {
return this.autoStartup;
}
@Override
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
public void setPhase(int phase) {
this.phase = phase;
}
@Override
public int getPhase() {
return this.phase;
}
}

View File

@@ -0,0 +1,318 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.pulsar.client.api.BatchReceivePolicy;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageListener;
import org.apache.pulsar.client.api.Messages;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.core.PulsarConsumerFactory;
import org.springframework.pulsar.event.ConsumerFailedToStartEvent;
import org.springframework.pulsar.event.ConsumerStartedEvent;
import org.springframework.pulsar.event.ConsumerStartingEvent;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.util.StringUtils;
import org.springframework.util.concurrent.ListenableFuture;
/**
* @author Soby Chacko
*/
public class DefaultPulsarMessageListenerContainer<T> extends AbstractPulsarMessageListenerContainer<T> {
private volatile boolean running = false;
private String beanName;
private volatile ListenableFuture<?> listenerConsumerFuture;
private volatile Listener listenerConsumer;
private volatile CountDownLatch startLatch = new CountDownLatch(1);
private final AbstractPulsarMessageListenerContainer thisOrParentContainer;
public DefaultPulsarMessageListenerContainer(PulsarConsumerFactory<? super T> pulsarConsumerFactory, PulsarContainerProperties pulsarContainerProperties) {
super(pulsarConsumerFactory, pulsarContainerProperties);
this.thisOrParentContainer = this;
}
@Override
public void start() {
try {
doStart();
}
catch (PulsarClientException e) {
e.printStackTrace(); //TODO: proper logging
}
}
private void doStart() throws PulsarClientException {
PulsarContainerProperties containerProperties = getPulsarContainerProperties();
Object messageListenerObject = containerProperties.getMessageListener();
AsyncListenableTaskExecutor consumerExecutor = containerProperties.getConsumerTaskExecutor();
@SuppressWarnings("unchecked")
MessageListener<T> messageListener = (MessageListener<T>) messageListenerObject;
if (consumerExecutor == null) {
consumerExecutor = new SimpleAsyncTaskExecutor(
(getBeanName() == null ? "" : getBeanName()) + "-C-");
containerProperties.setConsumerTaskExecutor(consumerExecutor);
}
this.listenerConsumer = new Listener(messageListener);
setRunning(true);
this.startLatch = new CountDownLatch(1);
this.listenerConsumerFuture = consumerExecutor.submitListenable(this.listenerConsumer);
try {
if (!this.startLatch.await(containerProperties.getConsumerStartTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
this.logger.error("Consumer thread failed to start - does the configured task executor "
+ "have enough threads to support all containers and concurrency?");
publishConsumerFailedToStart();
}
}
catch (@SuppressWarnings("UNUSED") InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void stop() {
setRunning(false);
System.out.println("Pausing this consumer.");
this.listenerConsumer.consumer.pause();
try {
System.out.println("Closing this consumer.");
this.listenerConsumer.consumer.close();
}
catch (PulsarClientException e) {
e.printStackTrace();
}
}
@Override
public boolean isRunning() {
return this.running;
}
protected void setRunning(boolean running) {
this.running = running;
}
/**
* Return the bean name.
*
* @return the bean name.
*/
@Nullable
public String getBeanName() {
return this.beanName;
}
@Override
public void destroy() {
}
private void publishConsumerStartingEvent() {
this.startLatch.countDown();
ApplicationEventPublisher publisher = getApplicationEventPublisher();
if (publisher != null) {
publisher.publishEvent(new ConsumerStartingEvent(this, this.thisOrParentContainer));
}
}
private void publishConsumerStartedEvent() {
ApplicationEventPublisher publisher = getApplicationEventPublisher();
if (publisher != null) {
publisher.publishEvent(new ConsumerStartedEvent(this, this.thisOrParentContainer));
}
}
private void publishConsumerFailedToStart() {
ApplicationEventPublisher publisher = getApplicationEventPublisher();
if (publisher != null) {
publisher.publishEvent(new ConsumerFailedToStartEvent(this, this.thisOrParentContainer));
}
}
private final class Listener implements SchedulingAwareRunnable {
private final MessageListener<T> listener;
private final PulsarBatchMessageListener<T> batchMessageHandler;
Consumer<T> consumer;
private final PulsarContainerProperties containerProperties = getPulsarContainerProperties();
private volatile Thread consumerThread;
@SuppressWarnings("unchecked")
Listener(MessageListener<?> messageListener) {
if (messageListener instanceof PulsarBatchMessageListener) {
this.batchMessageHandler = (PulsarBatchMessageListener<T>) messageListener;
this.listener = null;
}
else if (messageListener != null) {
this.listener = (MessageListener<T>) messageListener;
this.batchMessageHandler = null;
}
else {
this.listener = null;
this.batchMessageHandler = null;
}
try {
final PulsarContainerProperties pulsarContainerProperties = getPulsarContainerProperties();
Map<String, Object> propertiesToOverride = extractPropertiesToOverride(pulsarContainerProperties);
if (this.containerProperties.isBatchReceive() || this.containerProperties.isBatchAsyncReceive()) {
final BatchReceivePolicy batchReceivePolicy = BatchReceivePolicy.DEFAULT_POLICY;
this.consumer = getPulsarConsumerFactory().createConsumer(
(Schema) pulsarContainerProperties.getSchema(),
batchReceivePolicy, propertiesToOverride);
}
else if (this.containerProperties.isAsyncReceive()) {
this.consumer = getPulsarConsumerFactory().createConsumer(
(Schema) pulsarContainerProperties.getSchema(), propertiesToOverride);
}
else {
this.consumer = getPulsarConsumerFactory().createConsumer(
(Schema) pulsarContainerProperties.getSchema(), propertiesToOverride);
}
}
catch (PulsarClientException e) {
e.printStackTrace(); //TODO - Proper logging
}
}
private Map<String, Object> extractPropertiesToOverride(PulsarContainerProperties pulsarContainerProperties) {
final SubscriptionType subscriptionType = pulsarContainerProperties.getSubscriptionType();
final Map<String, Object> propertiesToOverride = new HashMap<>();
if (subscriptionType != null) {
propertiesToOverride.put("subscriptionType", subscriptionType);
}
final String[] topics = pulsarContainerProperties.getTopics();
final HashSet<String> strings = new HashSet<>(Arrays.stream(topics).toList());
if (!strings.isEmpty()) {
propertiesToOverride.put("topicNames", strings);
}
if (StringUtils.hasText(pulsarContainerProperties.getSubscriptionName())) {
propertiesToOverride.put("subscriptionName",
pulsarContainerProperties.getSubscriptionName());
}
return propertiesToOverride;
}
@Override
public boolean isLongLived() {
return true;
}
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public void run() {
publishConsumerStartingEvent();
this.consumerThread = Thread.currentThread();
publishConsumerStartedEvent();
while (isRunning()) {
Message<T> msg = null;
try {
// Wait for a message
if (this.containerProperties.isBatchReceive()) {
Messages<T> messages = consumer.batchReceive();
this.batchMessageHandler.received(consumer, messages);
consumer.acknowledge(messages);
}
else if (this.containerProperties.isBatchAsyncReceive()) {
final CompletableFuture<Messages<T>> messagesCompletableFuture = consumer.batchReceiveAsync();
messagesCompletableFuture.thenAccept(messages -> {
if (messages != null) {
this.batchMessageHandler.received(consumer, messages);
if (this.containerProperties.getAckMode() != PulsarContainerProperties.AckMode.MANUAL) {
try {
consumer.acknowledge(messages);
}
catch (PulsarClientException e) {
consumer.negativeAcknowledge(messages);
}
}
}
});
//consumer can do other things - but nothing ATM.
}
else if (this.containerProperties.isAsyncReceive()) {
final CompletableFuture<Message<T>> messageCompletableFuture = consumer.receiveAsync();
messageCompletableFuture.thenAccept(messages1 -> {
if (messages1 != null) {
this.listener.received(consumer, messages1);
if (this.containerProperties.getAckMode() != PulsarContainerProperties.AckMode.MANUAL) {
try {
consumer.acknowledge(messages1);
}
catch (PulsarClientException e) {
consumer.negativeAcknowledge(messages1);
}
}
}
});
//consumer can do other things - but nothing ATM.
//we may make this mode as the default rather than the sync mode below.
}
else {
try {
msg = consumer.receive();
this.listener.received(consumer, msg);
if (this.containerProperties.getAckMode() != PulsarContainerProperties.AckMode.MANUAL) {
consumer.acknowledge(msg);
}
}
catch (Exception e) {
consumer.negativeAcknowledge(msg);
}
}
}
catch (Exception e) {
// Message failed to process, redeliver later
consumer.negativeAcknowledge(msg);
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @author Soby Chacko
*/
package org.springframework.pulsar.listener;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageListener;
import org.apache.pulsar.client.api.Messages;
public interface PulsarBatchMessageListener<T> extends MessageListener<T> {
default void received(Consumer<T> consumer, Message<T> msg) {
throw new UnsupportedOperationException();
}
void received(Consumer<T> consumer, Messages<T> msg);
}

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener;
import java.util.SortedMap;
import java.util.regex.Pattern;
import org.apache.pulsar.client.api.ConsumerCryptoFailureAction;
import org.apache.pulsar.client.api.DeadLetterPolicy;
import org.apache.pulsar.client.api.RedeliveryBackoff;
import org.apache.pulsar.client.api.RegexSubscriptionMode;
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
import org.apache.pulsar.client.api.SubscriptionType;
/**
* @author Soby Chacko
*/
public class PulsarConsumerProperties {
/**
* Topic names.
*/
private String[] topics;
/**
* Topic pattern.
*/
private Pattern topicsPattern;
private String subscriptionName;
private SubscriptionType subscriptionType;
private int receiveQueueSize;
private long acknowledgementsGroupTimeMicros;
private long negativeAckRedeliveryDelayMicros;
private int maxTotalReceiverQueueSizeAcrossPartitions;
private String consumerName;
private long ackTimeoutMillis;
private long tickDurationMillis;
private int priorityLevel;
private ConsumerCryptoFailureAction cryptoFailureAction;
private SortedMap<String, String> properties;
private boolean readCompacted;
private SubscriptionInitialPosition subscriptionInitialPosition;
private int patternAutoDiscoveryPeriod;
private RegexSubscriptionMode regexSubscriptionMode;
private DeadLetterPolicy deadLetterPolicy;
private boolean autoUpdatePartitions;
private boolean replicateSubscriptionState;
private RedeliveryBackoff negativeAckRedeliveryBackoff;
private RedeliveryBackoff ackTimeoutRedeliveryBackoff;
private boolean autoAckOldestChunkedMessageOnQueueFull;
private int maxPendingChunkedMessage;
private long expireTimeOfIncompleteChunkedMessageMillis;
public PulsarConsumerProperties(String... topics) {
this.topics = topics.clone();
this.topicsPattern = null;
}
public PulsarConsumerProperties(Pattern topicPattern) {
this.topicsPattern = topicPattern;
this.topics = null;
}
public void setTopics(String[] topics) {
this.topics = topics;
}
public void setTopicsPattern(Pattern topicsPattern) {
this.topicsPattern = topicsPattern;
}
public String getSubscriptionName() {
return subscriptionName;
}
public void setSubscriptionName(String subscriptionName) {
this.subscriptionName = subscriptionName;
}
public int getReceiveQueueSize() {
return receiveQueueSize;
}
public void setReceiveQueueSize(int receiveQueueSize) {
this.receiveQueueSize = receiveQueueSize;
}
public SubscriptionType getSubscriptionType() {
return subscriptionType;
}
public void setSubscriptionType(SubscriptionType subscriptionType) {
this.subscriptionType = subscriptionType;
}
public long getAcknowledgementsGroupTimeMicros() {
return acknowledgementsGroupTimeMicros;
}
public void setAcknowledgementsGroupTimeMicros(long acknowledgementsGroupTimeMicros) {
this.acknowledgementsGroupTimeMicros = acknowledgementsGroupTimeMicros;
}
public long getNegativeAckRedeliveryDelayMicros() {
return negativeAckRedeliveryDelayMicros;
}
public void setNegativeAckRedeliveryDelayMicros(long negativeAckRedeliveryDelayMicros) {
this.negativeAckRedeliveryDelayMicros = negativeAckRedeliveryDelayMicros;
}
public int getMaxTotalReceiverQueueSizeAcrossPartitions() {
return maxTotalReceiverQueueSizeAcrossPartitions;
}
public void setMaxTotalReceiverQueueSizeAcrossPartitions(int maxTotalReceiverQueueSizeAcrossPartitions) {
this.maxTotalReceiverQueueSizeAcrossPartitions = maxTotalReceiverQueueSizeAcrossPartitions;
}
public String getConsumerName() {
return consumerName;
}
public void setConsumerName(String consumerName) {
this.consumerName = consumerName;
}
public long getAckTimeoutMillis() {
return ackTimeoutMillis;
}
public void setAckTimeoutMillis(long ackTimeoutMillis) {
this.ackTimeoutMillis = ackTimeoutMillis;
}
public long getTickDurationMillis() {
return tickDurationMillis;
}
public void setTickDurationMillis(long tickDurationMillis) {
this.tickDurationMillis = tickDurationMillis;
}
public int getPriorityLevel() {
return priorityLevel;
}
public void setPriorityLevel(int priorityLevel) {
this.priorityLevel = priorityLevel;
}
public ConsumerCryptoFailureAction getCryptoFailureAction() {
return cryptoFailureAction;
}
public void setCryptoFailureAction(ConsumerCryptoFailureAction cryptoFailureAction) {
this.cryptoFailureAction = cryptoFailureAction;
}
public SortedMap<String, String> getProperties() {
return properties;
}
public void setProperties(SortedMap<String, String> properties) {
this.properties = properties;
}
public boolean isReadCompacted() {
return readCompacted;
}
public void setReadCompacted(boolean readCompacted) {
this.readCompacted = readCompacted;
}
public SubscriptionInitialPosition getSubscriptionInitialPosition() {
return subscriptionInitialPosition;
}
public void setSubscriptionInitialPosition(SubscriptionInitialPosition subscriptionInitialPosition) {
this.subscriptionInitialPosition = subscriptionInitialPosition;
}
public int getPatternAutoDiscoveryPeriod() {
return patternAutoDiscoveryPeriod;
}
public void setPatternAutoDiscoveryPeriod(int patternAutoDiscoveryPeriod) {
this.patternAutoDiscoveryPeriod = patternAutoDiscoveryPeriod;
}
public RegexSubscriptionMode getRegexSubscriptionMode() {
return regexSubscriptionMode;
}
public void setRegexSubscriptionMode(RegexSubscriptionMode regexSubscriptionMode) {
this.regexSubscriptionMode = regexSubscriptionMode;
}
public DeadLetterPolicy getDeadLetterPolicy() {
return deadLetterPolicy;
}
public void setDeadLetterPolicy(DeadLetterPolicy deadLetterPolicy) {
this.deadLetterPolicy = deadLetterPolicy;
}
public boolean isAutoUpdatePartitions() {
return autoUpdatePartitions;
}
public void setAutoUpdatePartitions(boolean autoUpdatePartitions) {
this.autoUpdatePartitions = autoUpdatePartitions;
}
public boolean isReplicateSubscriptionState() {
return replicateSubscriptionState;
}
public void setReplicateSubscriptionState(boolean replicateSubscriptionState) {
this.replicateSubscriptionState = replicateSubscriptionState;
}
public RedeliveryBackoff getNegativeAckRedeliveryBackoff() {
return negativeAckRedeliveryBackoff;
}
public void setNegativeAckRedeliveryBackoff(RedeliveryBackoff negativeAckRedeliveryBackoff) {
this.negativeAckRedeliveryBackoff = negativeAckRedeliveryBackoff;
}
public RedeliveryBackoff getAckTimeoutRedeliveryBackoff() {
return ackTimeoutRedeliveryBackoff;
}
public void setAckTimeoutRedeliveryBackoff(RedeliveryBackoff ackTimeoutRedeliveryBackoff) {
this.ackTimeoutRedeliveryBackoff = ackTimeoutRedeliveryBackoff;
}
public boolean isAutoAckOldestChunkedMessageOnQueueFull() {
return autoAckOldestChunkedMessageOnQueueFull;
}
public void setAutoAckOldestChunkedMessageOnQueueFull(boolean autoAckOldestChunkedMessageOnQueueFull) {
this.autoAckOldestChunkedMessageOnQueueFull = autoAckOldestChunkedMessageOnQueueFull;
}
public int getMaxPendingChunkedMessage() {
return maxPendingChunkedMessage;
}
public void setMaxPendingChunkedMessage(int maxPendingChunkedMessage) {
this.maxPendingChunkedMessage = maxPendingChunkedMessage;
}
public long getExpireTimeOfIncompleteChunkedMessageMillis() {
return expireTimeOfIncompleteChunkedMessageMillis;
}
public void setExpireTimeOfIncompleteChunkedMessageMillis(long expireTimeOfIncompleteChunkedMessageMillis) {
this.expireTimeOfIncompleteChunkedMessageMillis = expireTimeOfIncompleteChunkedMessageMillis;
}
public String[] getTopics() {
return topics;
}
public Pattern getTopicsPattern() {
return topicsPattern;
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener;
import java.time.Duration;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.SubscriptionType;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public class PulsarContainerProperties extends PulsarConsumerProperties {
private static final Duration DEFAULT_CONSUMER_START_TIMEOUT = Duration.ofSeconds(30);
private Duration consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT;
public enum AckMode {
MANUAL;
}
private Schema<?> schema = Schema.BYTES;
private Object messageListener;
private AsyncListenableTaskExecutor consumerTaskExecutor;
private SubscriptionType subscriptionType;
private int maxNumMessages = -1;
private int maxNumBytes = 10 * 1024 * 1024;
private int batchTimeout = 100;
private boolean batchReceive;
private boolean batchAsyncReceive;
private boolean asyncReceive;
private AckMode ackMode;
public PulsarContainerProperties(String... topics) {
super(topics);
}
public Object getMessageListener() {
return messageListener;
}
public void setMessageListener(Object messageListener) {
this.messageListener = messageListener;
}
public AsyncListenableTaskExecutor getConsumerTaskExecutor() {
return this.consumerTaskExecutor;
}
public void setConsumerTaskExecutor(AsyncListenableTaskExecutor consumerExecutor) {
this.consumerTaskExecutor = consumerExecutor;
}
public SubscriptionType getSubscriptionType() {
return subscriptionType;
}
public void setSubscriptionType(SubscriptionType subscriptionType) {
this.subscriptionType = subscriptionType;
}
public int getMaxNumMessages() {
return maxNumMessages;
}
public void setMaxNumMessages(int maxNumMessages) {
this.maxNumMessages = maxNumMessages;
}
public int getMaxNumBytes() {
return maxNumBytes;
}
public void setMaxNumBytes(int maxNumBytes) {
this.maxNumBytes = maxNumBytes;
}
public int getBatchTimeout() {
return batchTimeout;
}
public void setBatchTimeout(int batchTimeout) {
this.batchTimeout = batchTimeout;
}
public boolean isBatchReceive() {
return batchReceive;
}
public void setBatchReceive(boolean batchReceive) {
this.batchReceive = batchReceive;
}
public boolean isBatchAsyncReceive() {
return batchAsyncReceive;
}
public void setBatchAsyncReceive(boolean batchAsyncReceive) {
this.batchAsyncReceive = batchAsyncReceive;
}
public boolean isAsyncReceive() {
return asyncReceive;
}
public void setAsyncReceive(boolean asyncReceive) {
this.asyncReceive = asyncReceive;
}
public AckMode getAckMode() {
return ackMode;
}
public void setAckMode(AckMode ackMode) {
this.ackMode = ackMode;
}
public Duration getConsumerStartTimeout() {
return this.consumerStartTimeout;
}
/**
* Set the timeout to wait for a consumer thread to start before logging
* an error. Default 30 seconds.
* @param consumerStartTimeout the consumer start timeout.
*/
public void setConsumerStartTimeout(Duration consumerStartTimeout) {
Assert.notNull(consumerStartTimeout, "'consumerStartTimout' cannot be null");
this.consumerStartTimeout = consumerStartTimeout;
}
public Schema<?> getSchema() {
return schema;
}
public void setSchema(Schema<?> schema) {
this.schema = schema;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener;
import java.util.Collection;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.listener.PulsarMessageListenerContainer;
/**
* @author Soby Chacko
*/
public interface PulsarListenerContainerRegistry {
@Nullable
PulsarMessageListenerContainer getListenerContainer(String id);
Set<String> getListenerContainerIds();
Collection<PulsarMessageListenerContainer> getListenerContainers();
Collection<PulsarMessageListenerContainer> getAllListenerContainers();
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.SmartLifecycle;
/**
* @author Soby Chacko
*/
public interface PulsarMessageListenerContainer extends SmartLifecycle, DisposableBean {
void setupMessageListener(Object messageListener);
@Override
default void destroy() {
stop();
}
default void setAutoStartup(boolean autoStartup) {
// empty
}
}

View File

@@ -0,0 +1,282 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener.adapter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.pulsar.PulsarException;
import org.springframework.pulsar.listener.adapter.InvocationResult;
import org.springframework.validation.Validator;
/**
* @author Soby Chacko
*/
public class DelegatingInvocableHandler {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private final List<InvocableHandlerMethod> handlers;
private final ConcurrentMap<Class<?>, InvocableHandlerMethod> cachedHandlers = new ConcurrentHashMap<>();
private final ConcurrentMap<InvocableHandlerMethod, MethodParameter> payloadMethodParameters =
new ConcurrentHashMap<>();
private final InvocableHandlerMethod defaultHandler;
private final Map<InvocableHandlerMethod, Expression> handlerSendTo = new ConcurrentHashMap<>();
private final Map<InvocableHandlerMethod, Boolean> handlerReturnsMessage = new ConcurrentHashMap<>();
private final Object bean;
private final BeanExpressionResolver resolver;
private final BeanExpressionContext beanExpressionContext;
private final ConfigurableListableBeanFactory beanFactory;
private final PayloadValidator validator;
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers,
@Nullable InvocableHandlerMethod defaultHandler, Object bean,
@Nullable BeanExpressionResolver beanExpressionResolver,
@Nullable BeanExpressionContext beanExpressionContext,
@Nullable BeanFactory beanFactory, @Nullable Validator validator) {
this.handlers = new ArrayList<>();
for (InvocableHandlerMethod handler : handlers) {
this.handlers.add(wrapIfNecessary(handler));
}
this.defaultHandler = wrapIfNecessary(defaultHandler);
this.bean = bean;
this.resolver = beanExpressionResolver;
this.beanExpressionContext = beanExpressionContext;
this.beanFactory = beanFactory instanceof ConfigurableListableBeanFactory
? (ConfigurableListableBeanFactory) beanFactory
: null;
this.validator = validator == null ? null : new PayloadValidator(validator);
}
@Nullable
private InvocableHandlerMethod wrapIfNecessary(@Nullable InvocableHandlerMethod handler) {
if (handler == null) {
return null;
}
Parameter[] parameters = handler.getMethod().getParameters();
for (Parameter parameter : parameters) {
// if (parameter.getType().equals(ConsumerRecordMetadata.class)) {
// return new DelegatingInvocableHandler.MetadataAwareInvocableHandlerMethod(handler);
// }
}
return handler;
}
/**
* Return the bean for this handler.
* @return the bean.
*/
public Object getBean() {
return this.bean;
}
/**
* Invoke the method with the given message.
* @param message the message.
* @param providedArgs additional arguments.
* @return the result of the invocation.
* @throws Exception raised if no suitable argument resolver can be found,
* or the method raised an exception.
*/
public Object invoke(Message<?> message, Object... providedArgs) throws Exception { //NOSONAR
Class<? extends Object> payloadClass = message.getPayload().getClass();
InvocableHandlerMethod handler = getHandlerForPayload(payloadClass);
if (this.validator != null && this.defaultHandler != null) {
MethodParameter parameter = this.payloadMethodParameters.get(handler);
if (parameter != null) {
this.validator.validate(message, parameter, message.getPayload());
}
}
Object result = null;
if (handler instanceof MetadataAwareInvocableHandlerMethod) {
// Object[] args = new Object[providedArgs.length + 1];
// args[0] = AdapterUtils.buildConsumerRecordMetadataFromArray(providedArgs);
// System.arraycopy(providedArgs, 0, args, 1, providedArgs.length);
// result = handler.invoke(message, args);
}
else {
result = handler.invoke(message, providedArgs);
}
Expression replyTo = this.handlerSendTo.get(handler);
return new InvocationResult(result, replyTo, this.handlerReturnsMessage.get(handler));
}
/**
* Determine the {@link InvocableHandlerMethod} for the provided type.
* @param payloadClass the payload class.
* @return the handler.
*/
protected InvocableHandlerMethod getHandlerForPayload(Class<? extends Object> payloadClass) {
InvocableHandlerMethod handler = this.cachedHandlers.get(payloadClass);
if (handler == null) {
handler = findHandlerForPayload(payloadClass);
if (handler == null) {
throw new PulsarException("No method found for " + payloadClass);
}
this.cachedHandlers.putIfAbsent(payloadClass, handler); //NOSONAR
//setupReplyTo(handler);
}
return handler;
}
@Nullable
protected InvocableHandlerMethod findHandlerForPayload(Class<? extends Object> payloadClass) {
InvocableHandlerMethod result = null;
for (InvocableHandlerMethod handler : this.handlers) {
if (matchHandlerMethod(payloadClass, handler)) {
if (result != null) {
boolean resultIsDefault = result.equals(this.defaultHandler);
if (!handler.equals(this.defaultHandler) && !resultIsDefault) {
throw new PulsarException("Ambiguous methods for payload type: " + payloadClass + ": " +
result.getMethod().getName() + " and " + handler.getMethod().getName());
}
if (!resultIsDefault) {
continue; // otherwise replace the result with the actual match
}
}
result = handler;
}
}
return result != null ? result : this.defaultHandler;
}
protected boolean matchHandlerMethod(Class<? extends Object> payloadClass, InvocableHandlerMethod handler) {
Method method = handler.getMethod();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
// Single param; no annotation or not @Header
if (parameterAnnotations.length == 1) {
MethodParameter methodParameter = new MethodParameter(method, 0);
if ((methodParameter.getParameterAnnotations().length == 0
|| !methodParameter.hasParameterAnnotation(Header.class))
&& methodParameter.getParameterType().isAssignableFrom(payloadClass)) {
if (this.validator != null) {
this.payloadMethodParameters.put(handler, methodParameter);
}
return true;
}
}
MethodParameter foundCandidate = findCandidate(payloadClass, method, parameterAnnotations);
if (foundCandidate != null && this.validator != null) {
this.payloadMethodParameters.put(handler, foundCandidate);
}
return foundCandidate != null;
}
private MethodParameter findCandidate(Class<? extends Object> payloadClass, Method method,
Annotation[][] parameterAnnotations) {
MethodParameter foundCandidate = null;
for (int i = 0; i < parameterAnnotations.length; i++) {
MethodParameter methodParameter = new MethodParameter(method, i);
if ((methodParameter.getParameterAnnotations().length == 0
|| !methodParameter.hasParameterAnnotation(Header.class))
&& methodParameter.getParameterType().isAssignableFrom(payloadClass)) {
if (foundCandidate != null) {
throw new PulsarException("Ambiguous payload parameter for " + method.toGenericString());
}
foundCandidate = methodParameter;
}
}
return foundCandidate;
}
/**
* Return a string representation of the method that will be invoked for this payload.
* @param payload the payload.
* @return the method name.
*/
public String getMethodNameFor(Object payload) {
InvocableHandlerMethod handlerForPayload = getHandlerForPayload(payload.getClass());
return handlerForPayload == null ? "no match" : handlerForPayload.getMethod().toGenericString(); //NOSONAR
}
public boolean hasDefaultHandler() {
return this.defaultHandler != null;
}
/**
* A handler method that is aware of metadata.
*
* @since 2.5
*/
private static final class MetadataAwareInvocableHandlerMethod extends InvocableHandlerMethod {
MetadataAwareInvocableHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
}
private static final class PayloadValidator extends PayloadMethodArgumentResolver {
PayloadValidator(Validator validator) {
super(new MessageConverter() { // Required but never used
@Override
@Nullable
public Message<?> toMessage(Object payload, @Nullable
MessageHeaders headers) {
return null;
}
@Override
@Nullable
public Object fromMessage(Message<?> message, Class<?> targetClass) {
return null;
}
}, validator);
}
@Override
public void validate(Message<?> message, MethodParameter parameter, Object target) { // NOSONAR - public
super.validate(message, parameter, target);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener.adapter;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
/**
* @author Soby Chacko
*/
public class HandlerAdapter {
private final InvocableHandlerMethod invokerHandlerMethod;
private final DelegatingInvocableHandler delegatingHandler;
/**
* Construct an instance with the provided method.
* @param invokerHandlerMethod the method.
*/
public HandlerAdapter(InvocableHandlerMethod invokerHandlerMethod) {
this.invokerHandlerMethod = invokerHandlerMethod;
this.delegatingHandler = null;
}
/**
* Construct an instance with the provided delegating handler.
* @param delegatingHandler the handler.
*/
public HandlerAdapter(DelegatingInvocableHandler delegatingHandler) {
this.invokerHandlerMethod = null;
this.delegatingHandler = delegatingHandler;
}
public Object invoke(Message<?> message, Object... providedArgs) throws Exception { //NOSONAR
if (this.invokerHandlerMethod != null) {
return this.invokerHandlerMethod.invoke(message, providedArgs); // NOSONAR
}
else if (this.delegatingHandler.hasDefaultHandler()) {
// Needed to avoid returning raw Message which matches Object
Object[] args = new Object[providedArgs.length + 1];
args[0] = message.getPayload();
System.arraycopy(providedArgs, 0, args, 1, providedArgs.length);
return this.delegatingHandler.invoke(message, args);
}
else {
return this.delegatingHandler.invoke(message, providedArgs);
}
}
public String getMethodAsString(Object payload) {
if (this.invokerHandlerMethod != null) {
return this.invokerHandlerMethod.getMethod().toGenericString();
}
else {
return this.delegatingHandler.getMethodNameFor(payload);
}
}
public Object getBean() {
if (this.invokerHandlerMethod != null) {
return this.invokerHandlerMethod.getBean();
}
else {
return this.delegatingHandler.getBean();
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener.adapter;
import org.springframework.expression.Expression;
import org.springframework.lang.Nullable;
/**
* @author Soby Chacko
*/
public final class InvocationResult {
@Nullable
private final Object result;
@Nullable
private final Expression sendTo;
private final boolean messageReturnType;
public InvocationResult(@Nullable Object result, @Nullable Expression sendTo, boolean messageReturnType) {
this.result = result;
this.sendTo = sendTo;
this.messageReturnType = messageReturnType;
}
@Nullable
public Object getResult() {
return this.result;
}
@Nullable
public Expression getSendTo() {
return this.sendTo;
}
public boolean isMessageReturnType() {
return this.messageReturnType;
}
@Override
public String toString() {
return "InvocationResult [result=" + this.result
+ ", sendTo=" + (this.sendTo == null ? "null" : this.sendTo.getExpressionString())
+ ", messageReturnType=" + this.messageReturnType + "]";
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener.adapter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Messages;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.pulsar.support.converter.PulsarBatchMessageConverter;
import org.springframework.pulsar.listener.PulsarBatchMessageListener;
import org.springframework.pulsar.support.converter.PulsarBatchMessagingMessageConverter;
import org.springframework.pulsar.support.converter.PulsarRecordMessageConverter;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public class PulsarBatchMessagingMessageListenerAdapter <V> extends PulsarMessagingMessageListenerAdapter<V>
implements PulsarBatchMessageListener<V> {
private PulsarBatchMessageConverter<V> batchMessageConverter = new PulsarBatchMessagingMessageConverter<V>();
public PulsarBatchMessagingMessageListenerAdapter(Object bean, Method method) {
super(bean, method);
}
public void setBatchMessageConverter(PulsarBatchMessageConverter messageConverter) {
Assert.notNull(messageConverter, "'messageConverter' cannot be null");
this.batchMessageConverter = messageConverter;
PulsarRecordMessageConverter recordMessageConverter = messageConverter.getRecordMessageConverter();
if (recordMessageConverter != null) {
setMessageConverter(recordMessageConverter);
}
}
protected final PulsarBatchMessageConverter getBatchMessageConverter() {
return this.batchMessageConverter;
}
public void received(Consumer<V> consumer, Messages<V> msg) {
Message<?> message;
if (!isConsumerRecordList()) {
if (isMessageList()) {
List<Message<?>> messages = new ArrayList<>(msg.size());
for (org.apache.pulsar.client.api.Message<V> record : msg) {
messages.add(toMessagingMessage(record, consumer));
}
message = MessageBuilder.withPayload(messages).build();
}
else {
message = toMessagingMessage(msg, consumer);
}
}
else {
message = null; // optimization since we won't need any conversion to invoke
}
logger.debug(() -> "Processing [" + message + "]");
invoke(msg, consumer, message);
}
protected void invoke(Object records, Consumer<V> consumer,
final Message<?> messageArg) {
Message<?> message = messageArg;
try {
Object result = invokeHandler(records, message, consumer);
// if (result != null) {
// handleResult(result, records, message);
// }
}
catch (Exception e) {
throw e;
}
}
protected Message<?> toMessagingMessage(Messages<V> msg, Consumer<V> consumer) {
return getBatchMessageConverter().toMessage(msg, consumer, getType());
}
}

View File

@@ -0,0 +1,288 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener.adapter;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.List;
import org.apache.commons.logging.LogFactory;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.Messages;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.MethodParameter;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.pulsar.support.converter.PulsarMessagingMessageConverter;
import org.springframework.pulsar.support.converter.PulsarRecordMessageConverter;
import org.springframework.util.Assert;
/**
* @author Soby Chacko
*/
public abstract class PulsarMessagingMessageListenerAdapter<V> {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private final Object bean;
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
private final Type inferredType;
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
private HandlerAdapter handlerMethod;
private boolean conversionNeeded = true;
private boolean messageReturnType;
private boolean isConsumerRecordList;
private boolean isMessageList;
private boolean isConsumerRecords;
private boolean converterSet;
private PulsarRecordMessageConverter messageConverter = new PulsarMessagingMessageConverter<V>();
private Type fallbackType = Object.class;
public PulsarMessagingMessageListenerAdapter(Object bean, Method method) {
this.bean = bean;
this.inferredType = determineInferredType(method);
}
public void setMessageConverter(PulsarRecordMessageConverter messageConverter) {
this.messageConverter = messageConverter;
this.converterSet = true;
}
protected final PulsarRecordMessageConverter getMessageConverter() {
return this.messageConverter;
}
public void setMessagingConverter(SmartMessageConverter messageConverter) {
Assert.isTrue(!this.converterSet, "Cannot set the SmartMessageConverter when setting the messageConverter, "
+ "add the SmartConverter to the message converter instead");
((PulsarMessagingMessageConverter<V>) this.messageConverter).setMessagingConverter(messageConverter);
}
protected Type getType() {
return this.inferredType == null ? this.fallbackType : this.inferredType;
}
public void setFallbackType(Class<?> fallbackType) {
this.fallbackType = fallbackType;
}
public void setHandlerMethod(HandlerAdapter handlerMethod) {
this.handlerMethod = handlerMethod;
}
protected boolean isConsumerRecordList() {
return this.isConsumerRecordList;
}
public boolean isConsumerRecords() {
return this.isConsumerRecords;
}
public boolean isConversionNeeded() {
return this.conversionNeeded;
}
public void setBeanResolver(BeanResolver beanResolver) {
this.evaluationContext.setBeanResolver(beanResolver);
this.evaluationContext.setTypeConverter(new StandardTypeConverter());
this.evaluationContext.addPropertyAccessor(new MapAccessor());
}
protected boolean isMessageList() {
return this.isMessageList;
}
protected org.springframework.messaging.Message<?> toMessagingMessage(Message<V> record, Consumer<V> consumer) {
return getMessageConverter().toMessage(record, consumer, getType());
}
protected final Object invokeHandler(Object data, org.springframework.messaging.Message<?> message,
Consumer<V> consumer) {
try {
if (data instanceof List && !this.isConsumerRecordList) {
return this.handlerMethod.invoke(message, consumer);
}
else {
return this.handlerMethod.invoke(message, data, consumer);
}
}
catch (Exception ex) {
throw new MessageConversionException("Cannot handle message", ex);
}
}
protected Type determineInferredType(Method method) { // NOSONAR complexity
if (method == null) {
return null;
}
Type genericParameterType = null;
int allowedBatchParameters = 1;
int notConvertibleParameters = 0;
for (int i = 0; i < method.getParameterCount(); i++) {
MethodParameter methodParameter = new MethodParameter(method, i);
/*
* We're looking for a single non-annotated parameter, or one annotated with @Payload.
* We ignore parameters with type Message, Consumer, Ack, ConsumerRecord because they
* are not involved with conversion.
*/
Type parameterType = methodParameter.getGenericParameterType();
boolean isNotConvertible = parameterIsType(parameterType, Message.class);
boolean isConsumer = parameterIsType(parameterType, Consumer.class);
if (isNotConvertible) {
notConvertibleParameters++;
}
if (!isNotConvertible && !isMessageWithNoTypeInfo(parameterType)
&& (methodParameter.getParameterAnnotations().length == 0
|| methodParameter.hasParameterAnnotation(Payload.class))) {
if (genericParameterType == null) {
genericParameterType = extractGenericParameterTypFromMethodParameter(methodParameter);
}
else {
this.logger.debug(() -> "Ambiguous parameters for target payload for method " + method
+ "; no inferred type available");
break;
}
}
else {
if (isConsumer) {
allowedBatchParameters++;
}
else {
if (parameterType instanceof ParameterizedType
&& ((ParameterizedType) parameterType).getRawType().equals(Consumer.class)) {
allowedBatchParameters++;
}
}
}
}
if (notConvertibleParameters == method.getParameterCount() && method.getReturnType().equals(void.class)) {
this.conversionNeeded = false;
}
boolean validParametersForBatch = method.getGenericParameterTypes().length <= allowedBatchParameters;
if (!validParametersForBatch) {
String stateMessage = "A parameter of type '%s' must be the only parameter "
+ "(except for an optional 'Acknowledgment' and/or 'Consumer' "
+ "and/or '@Header(KafkaHeaders.GROUP_ID) String groupId'";
}
this.messageReturnType = returnTypeMessageOrCollectionOf(method);
return genericParameterType;
}
private Type extractGenericParameterTypFromMethodParameter(MethodParameter methodParameter) {
Type genericParameterType = methodParameter.getGenericParameterType();
if (genericParameterType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericParameterType;
if (parameterizedType.getRawType().equals(org.springframework.messaging.Message.class)) {
genericParameterType = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0];
}
else if (parameterizedType.getRawType().equals(List.class)
&& parameterizedType.getActualTypeArguments().length == 1) {
Type paramType = parameterizedType.getActualTypeArguments()[0];
this.isConsumerRecordList = paramType.equals(Messages.class);
boolean messageHasGeneric = paramType instanceof ParameterizedType
&& ((ParameterizedType) paramType).getRawType().equals(org.springframework.messaging.Message.class);
this.isMessageList = paramType.equals(org.springframework.messaging.Message.class) || messageHasGeneric;
if (messageHasGeneric) {
genericParameterType = ((ParameterizedType) paramType).getActualTypeArguments()[0];
}
}
else {
this.isConsumerRecords = parameterizedType.getRawType().equals(Messages.class);
}
}
return genericParameterType;
}
public static boolean returnTypeMessageOrCollectionOf(Method method) {
Type returnType = method.getGenericReturnType();
if (returnType.equals(org.springframework.messaging.Message.class)) {
return true;
}
if (returnType instanceof ParameterizedType) {
ParameterizedType prt = (ParameterizedType) returnType;
Type rawType = prt.getRawType();
if (rawType.equals(org.springframework.messaging.Message.class)) {
return true;
}
if (rawType.equals(Collection.class)) {
Type collectionType = prt.getActualTypeArguments()[0];
if (collectionType.equals(org.springframework.messaging.Message.class)) {
return true;
}
return collectionType instanceof ParameterizedType
&& ((ParameterizedType) collectionType).getRawType().equals(org.springframework.messaging.Message.class);
}
}
return false;
}
private boolean parameterIsType(Type parameterType, Type type) {
if (parameterType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) parameterType;
Type rawType = parameterizedType.getRawType();
if (rawType.equals(type)) {
return true;
}
}
return parameterType.equals(type);
}
private boolean isMessageWithNoTypeInfo(Type parameterType) {
if (parameterType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) parameterType;
Type rawType = parameterizedType.getRawType();
if (rawType.equals(org.springframework.messaging.Message.class)) {
return parameterizedType.getActualTypeArguments()[0] instanceof WildcardType;
}
}
return parameterType.equals(org.springframework.messaging.Message.class); // could be Message without a generic type
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.listener.adapter;
import java.lang.reflect.Method;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageListener;
/**
* @author Soby Chacko
*/
public class PulsarRecordMessagingMessageListenerAdapter<V> extends PulsarMessagingMessageListenerAdapter<V>
implements MessageListener<V> {
public PulsarRecordMessagingMessageListenerAdapter(Object bean, Method method) {
super(bean, method);
}
@Override
public void received(Consumer<V> consumer, Message<V> record) {
org.springframework.messaging.Message<?> message = null;
if (isConversionNeeded()) {
message = toMessagingMessage(record, consumer);
}
else {
//message = NULL_MESSAGE;
}
if (logger.isDebugEnabled()) {
this.logger.debug("Processing [" + message + "]");
}
try {
Object result = invokeHandler(record, message, consumer);
if (result != null) {
//handleResult(result, record, message);
}
}
catch (Exception e) { // NOSONAR ex flow control
throw e;
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* @author Soby Chacko
*/
public class EndpointHandlerMethod {
private final Object beanOrClass;
private final String methodName;
private Object bean;
private Method method;
public EndpointHandlerMethod(Object beanOrClass, String methodName) {
Assert.notNull(beanOrClass, () -> "No destination bean or class provided!");
Assert.notNull(methodName, () -> "No method name for destination bean class provided!");
this.beanOrClass = beanOrClass;
this.methodName = methodName;
}
public EndpointHandlerMethod(Object bean, Method method) {
Assert.notNull(bean, () -> "No bean for destination provided!");
Assert.notNull(method, () -> "No method for destination bean class provided!");
this.method = method;
this.bean = bean;
this.beanOrClass = bean.getClass();
this.methodName = method.getName();
}
/**
* Return the method.
* @return the method.
*/
public Method getMethod() {
if (this.beanOrClass instanceof Class) {
return forClass((Class<?>) this.beanOrClass);
}
Assert.state(this.bean != null, "Bean must be resolved before accessing its method");
if (this.bean instanceof EndpointHandlerMethod) {
try {
return Object.class.getMethod("toString");
}
catch (NoSuchMethodException | SecurityException e) {
}
}
return forClass(this.bean.getClass());
}
public String getMethodName() {
Assert.state(this.methodName != null, "Unexpected call to getMethodName()");
return this.methodName;
}
public Object resolveBean(BeanFactory beanFactory) {
if (this.bean instanceof EndpointHandlerMethod) {
return ((EndpointHandlerMethod) this.bean).beanOrClass;
}
if (this.bean == null) {
try {
if (this.beanOrClass instanceof Class) {
Class<?> clazz = (Class<?>) this.beanOrClass;
try {
this.bean = beanFactory.getBean(clazz);
}
catch (NoSuchBeanDefinitionException e) {
String beanName = clazz.getSimpleName() + "-handlerMethod";
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(beanName,
new RootBeanDefinition(clazz));
this.bean = beanFactory.getBean(beanName);
}
}
else {
String beanName = (String) this.beanOrClass;
this.bean = beanFactory.getBean(beanName);
}
}
catch (BeanCurrentlyInCreationException ex) {
this.bean = this;
}
}
return this.bean;
}
private Method forClass(Class<?> clazz) {
if (this.method == null) {
this.method = Arrays.stream(ReflectionUtils.getDeclaredMethods(clazz))
.filter(mthd -> mthd.getName().equals(this.methodName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
String.format("No method %s in class %s", this.methodName, clazz)));
}
return this.method;
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Soby Chacko
*/
public final class JavaUtils {
/**
* The singleton instance of this utility class.
*/
public static final JavaUtils INSTANCE = new JavaUtils();
private JavaUtils() {
}
/**
* Invoke {@link Consumer#accept(Object)} with the value if the condition is true.
*
* @param condition the condition.
* @param value the value.
* @param consumer the consumer.
* @param <T> the value type.
* @return this.
*/
public <T> JavaUtils acceptIfCondition(boolean condition, T value, Consumer<T> consumer) {
if (condition) {
consumer.accept(value);
}
return this;
}
/**
* Invoke {@link Consumer#accept(Object)} with the value if it is not null.
*
* @param value the value.
* @param consumer the consumer.
* @param <T> the value type.
* @return this.
*/
public <T> JavaUtils acceptIfNotNull(@Nullable T value, Consumer<T> consumer) {
if (value != null) {
consumer.accept(value);
}
return this;
}
/**
* Invoke {@link Consumer#accept(Object)} with the value if it is not null or empty.
*
* @param value the value.
* @param consumer the consumer.
* @return this.
*/
public JavaUtils acceptIfHasText(String value, Consumer<String> consumer) {
if (StringUtils.hasText(value)) {
consumer.accept(value);
}
return this;
}
/**
* Invoke {@link Consumer#accept(Object)} with the cast value if the object is an
* instance of the provided class.
*
* @param <T> the type of the class to check and cast.
* @param type the type.
* @param value the value to be checked and cast.
* @param consumer the consumer.
* @return this.
* @since 2.9
*/
@SuppressWarnings("unchecked")
public <T> JavaUtils acceptIfInstanceOf(Class<T> type, Object value, Consumer<T> consumer) {
if (type.isAssignableFrom(value.getClass())) {
consumer.accept((T) value);
}
return this;
}
/**
* Invoke {@link Consumer#accept(Object)} with the value if it is not null or empty.
*
* @param value the value.
* @param consumer the consumer.
* @param <T> the value type.
* @return this.
*/
public <T> JavaUtils acceptIfNotEmpty(List<T> value, Consumer<List<T>> consumer) {
if (!CollectionUtils.isEmpty(value)) {
consumer.accept(value);
}
return this;
}
/**
* Invoke {@link Consumer#accept(Object)} with the value if it is not null or empty.
*
* @param value the value.
* @param consumer the consumer.
* @param <T> the value type.
* @return this.
*/
public <T> JavaUtils acceptIfNotEmpty(T[] value, Consumer<T[]> consumer) {
if (!ObjectUtils.isEmpty(value)) {
consumer.accept(value);
}
return this;
}
/**
* Invoke {@link BiConsumer#accept(Object, Object)} with the arguments if the
* condition is true.
*
* @param condition the condition.
* @param t1 the first consumer argument
* @param t2 the second consumer argument
* @param consumer the consumer.
* @param <T1> the first argument type.
* @param <T2> the second argument type.
* @return this.
*/
public <T1, T2> JavaUtils acceptIfCondition(boolean condition, T1 t1, T2 t2, BiConsumer<T1, T2> consumer) {
if (condition) {
consumer.accept(t1, t2);
}
return this;
}
/**
* Invoke {@link BiConsumer#accept(Object, Object)} with the arguments if the t2
* argument is not null.
*
* @param t1 the first argument
* @param t2 the second consumer argument
* @param consumer the consumer.
* @param <T1> the first argument type.
* @param <T2> the second argument type.
* @return this.
*/
public <T1, T2> JavaUtils acceptIfNotNull(T1 t1, T2 t2, BiConsumer<T1, T2> consumer) {
if (t2 != null) {
consumer.accept(t1, t2);
}
return this;
}
/**
* Invoke {@link BiConsumer#accept(Object, Object)} with the arguments if the value
* argument is not null or empty.
*
* @param t1 the first consumer argument.
* @param value the second consumer argument
* @param <T> the first argument type.
* @param consumer the consumer.
* @return this.
*/
public <T> JavaUtils acceptIfHasText(T t1, String value, BiConsumer<T, String> consumer) {
if (StringUtils.hasText(value)) {
consumer.accept(t1, value);
}
return this;
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support;
/**
* @author Soby Chacko
*/
public interface MessageConverter {
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support.converter;
import java.lang.reflect.Type;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Messages;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.pulsar.support.MessageConverter;
/**
* @author Soby Chacko
*/
public interface PulsarBatchMessageConverter<T> extends MessageConverter {
@NonNull
Message<?> toMessage(Messages<T> records, Consumer<T> consumer, Type payloadType);
T fromMessage(Messages<T> message, String defaultTopic);
@Nullable
default PulsarRecordMessageConverter<T> getRecordMessageConverter() {
return null;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support.converter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Messages;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.pulsar.support.converter.PulsarBatchMessageConverter;
import org.springframework.pulsar.support.converter.PulsarRecordMessageConverter;
/**
* @author Soby Chacko
*/
public class PulsarBatchMessagingMessageConverter<T> implements PulsarBatchMessageConverter<T> {
private final PulsarRecordMessageConverter recordConverter;
public PulsarBatchMessagingMessageConverter() {
this(null);
}
public PulsarBatchMessagingMessageConverter(PulsarRecordMessageConverter recordConverter) {
this.recordConverter = recordConverter;
}
@Override
public Message<?> toMessage(Messages<T> records, Consumer<T> consumer, Type type) {
List<Object> payloads = new ArrayList<>();
List<Exception> conversionFailures = new ArrayList<>();
for (org.apache.pulsar.client.api.Message<T> message : records) {
payloads.add(obtainPayload(type, message, conversionFailures));
}
return MessageBuilder.createMessage(payloads, new MessageHeaders(Collections.emptyMap()));
}
private Object obtainPayload(Type type, org.apache.pulsar.client.api.Message<T> record, List<Exception> conversionFailures) {
return this.recordConverter == null || !containerType(type)
? extractAndConvertValue(record, type)
: convert(record, type, conversionFailures);
}
private boolean containerType(Type type) {
return type instanceof ParameterizedType
&& ((ParameterizedType) type).getActualTypeArguments().length == 1;
}
protected Object extractAndConvertValue(org.apache.pulsar.client.api.Message<T> record, Type type) {
return record.getValue();
}
protected Object convert(org.apache.pulsar.client.api.Message<T> record, Type type, List<Exception> conversionFailures) {
try {
Object payload = this.recordConverter
.toMessage(record, null, ((ParameterizedType) type).getActualTypeArguments()[0]).getPayload();
conversionFailures.add(null);
return payload;
}
catch (Exception ex) {
throw new RuntimeException("The batch converter can only report conversion failures to the listener "
+ "if the record.value() is byte[], Bytes, or String", ex);
}
}
@Override
public T fromMessage(Messages<T> message, String defaultTopic) {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support.converter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import org.apache.pulsar.client.api.Consumer;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
/**
* @author Soby Chacko
*/
public class PulsarMessagingMessageConverter<V> implements PulsarRecordMessageConverter<V> {
private SmartMessageConverter messagingConverter;
@Override
public Message<?> toMessage(org.apache.pulsar.client.api.Message<V> record, Consumer<V> consumer, Type type) {
Message<?> message = MessageBuilder.createMessage(extractAndConvertValue(record, type), new MessageHeaders(Collections.emptyMap()));
if (this.messagingConverter != null) {
Class<?> clazz = type instanceof Class ? (Class<?>) type : type instanceof ParameterizedType
? (Class<?>) ((ParameterizedType) type).getRawType() : Object.class;
Object payload = this.messagingConverter.fromMessage(message, clazz, type);
if (payload != null) {
message = new GenericMessage<>(payload, message.getHeaders());
}
}
return message;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public V fromMessage(Message<?> messageArg, String defaultTopic) {
Message<?> message = messageArg;
if (this.messagingConverter != null) {
Message<?> converted = this.messagingConverter.toMessage(message.getPayload(), message.getHeaders());
if (converted != null) {
message = converted;
}
}
return null; //TODO
}
protected org.springframework.messaging.converter.MessageConverter getMessagingConverter() {
return this.messagingConverter;
}
public void setMessagingConverter(SmartMessageConverter messagingConverter) {
this.messagingConverter = messagingConverter;
}
protected Object extractAndConvertValue(org.apache.pulsar.client.api.Message<V> record, Type type) {
return record.getValue();
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.support.converter;
import java.lang.reflect.Type;
import org.apache.pulsar.client.api.Consumer;
import org.springframework.lang.NonNull;
import org.springframework.messaging.Message;
import org.springframework.pulsar.support.MessageConverter;
/**
* @author Soby Chacko
*/
public interface PulsarRecordMessageConverter<T> extends MessageConverter {
@NonNull
Message<?> toMessage(org.apache.pulsar.client.api.Message<T> record, Consumer<T> consumer,
Type payloadType);
T fromMessage(Message<?> message, String defaultTopic);
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration:\
org.springframework.pulsar.autoconfig.PulsarAutoConfiguration

View File

@@ -0,0 +1,25 @@
package org.springframework.pulsar.core;
import org.testcontainers.containers.PulsarContainer;
import org.testcontainers.utility.DockerImageName;
abstract class AbstractContainerBaseTest {
static final DockerImageName PULSAR_IMAGE = DockerImageName.parse("apachepulsar/pulsar:2.10.0");
static PulsarContainer PULSAR_CONTAINER;
static {
PULSAR_CONTAINER = new PulsarContainer(PULSAR_IMAGE);
PULSAR_CONTAINER.start();
}
protected static String getPulsarBrokerUrl() {
return PULSAR_CONTAINER.getPulsarBrokerUrl();
}
protected static String getHttpServiceUrl() {
return PULSAR_CONTAINER.getHttpServiceUrl();
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.MessageListener;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.Schema;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PulsarContainer;
import org.testcontainers.utility.DockerImageName;
import org.springframework.pulsar.listener.DefaultPulsarMessageListenerContainer;
import org.springframework.pulsar.listener.PulsarContainerProperties;
/**
* @author Soby Chacko
*/
public class DefaultConsumerTests extends AbstractContainerBaseTest {
@Test
public void testDefaultConsumer() throws Exception {
// try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE)) {
// pulsar.start();
Map<String, Object> config = new HashMap<>();
final HashSet<String> strings = new HashSet<String>();
strings.add("foobar-012");
config.put("topicNames", strings);
config.put("subscriptionName", "foobar-sb-012");
final PulsarClient pulsarClient = PulsarClient.builder()
.serviceUrl(getPulsarBrokerUrl())
.build();
final DefaultPulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, config);
CountDownLatch latch = new CountDownLatch(1);
PulsarContainerProperties pulsarContainerProperties = new PulsarContainerProperties();
pulsarContainerProperties.setMessageListener(
(MessageListener<?>) (consumer, msg) -> latch.countDown());
pulsarContainerProperties.setSchema(Schema.STRING);
DefaultPulsarMessageListenerContainer<String> container = new DefaultPulsarMessageListenerContainer<>(
pulsarConsumerFactory, pulsarContainerProperties);
container.start();
Map<String, Object> prodConfig = new HashMap<>();
prodConfig.put("topicName", "foobar-012");
final DefaultPulsarProducerFactory<String> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(pulsarClient, prodConfig);
final PulsarTemplate<String> pulsarTemplate = new PulsarTemplate<>(pulsarProducerFactory);
final CompletableFuture<MessageId> future = pulsarTemplate.sendAsync("hello john doe");
latch.await(10, TimeUnit.SECONDS);
pulsarClient.close();
// }
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageListener;
import org.apache.pulsar.client.api.MessageRouter;
import org.apache.pulsar.client.api.MessageRoutingMode;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.SubscriptionType;
import org.apache.pulsar.client.api.TopicMetadata;
import org.junit.jupiter.api.Test;
import org.springframework.pulsar.listener.DefaultPulsarMessageListenerContainer;
import org.springframework.pulsar.listener.PulsarContainerProperties;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* @author Soby Chacko
*/
public class FailoverConsumerTests extends AbstractContainerBaseTest {
@Test
public void testFailOverConsumersOnPartitionedTopic() throws Exception {
PulsarAdmin admin = PulsarAdmin.builder()
.serviceHttpUrl(getHttpServiceUrl())
.build();
String topicName = "persistent://public/default/my-part-topic-1";
int numPartitions = 3;
admin.topics().createPartitionedTopic(topicName, numPartitions);
Map<String, Object> config = new HashMap<>();
final HashSet<String> topics = new HashSet<>();
topics.add("my-part-topic-1");
config.put("topicNames", topics);
config.put("subscriptionName", "my-part-subscription-1");
final PulsarClient pulsarClient = PulsarClient.builder()
.serviceUrl(getPulsarBrokerUrl())
.build();
final DefaultPulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, config);
CountDownLatch latch = new CountDownLatch(3);
PulsarContainerProperties pulsarContainerProperties = new PulsarContainerProperties();
pulsarContainerProperties.setMessageListener(new MessageListener() {
@Override
public void received(Consumer consumer, Message msg) {
latch.countDown();
}
});
pulsarContainerProperties.setSubscriptionType(SubscriptionType.Failover);
pulsarContainerProperties.setSchema(Schema.STRING);
DefaultPulsarMessageListenerContainer<String> container = new DefaultPulsarMessageListenerContainer(
pulsarConsumerFactory, pulsarContainerProperties);
container.start();
DefaultPulsarMessageListenerContainer<String> container1 = new DefaultPulsarMessageListenerContainer(
pulsarConsumerFactory, pulsarContainerProperties);
container1.start();
DefaultPulsarMessageListenerContainer<String> container2 = new DefaultPulsarMessageListenerContainer(
pulsarConsumerFactory, pulsarContainerProperties);
container2.start();
Map<String, Object> prodConfig = new HashMap<>();
prodConfig.put("topicName", "my-part-topic-1");
prodConfig.put("messageRoutingMode", MessageRoutingMode.CustomPartition);
final DefaultPulsarProducerFactory<String> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(pulsarClient, prodConfig);
final PulsarTemplate<String> pulsarTemplate = new PulsarTemplate<>(pulsarProducerFactory);
pulsarTemplate.sendAsync("hello john doe", new FooRouter());
pulsarTemplate.sendAsync("hello alice doe", new BarRouter());
pulsarTemplate.sendAsync("hello buzz doe", new BuzzRouter());
final boolean await = latch.await(10, TimeUnit.SECONDS);
assertThat(await).isTrue();
}
static class FooRouter implements MessageRouter {
@Override
public int choosePartition(Message<?> msg, TopicMetadata metadata) {
return 0;
}
}
static class BarRouter implements MessageRouter {
@Override
public int choosePartition(Message<?> msg, TopicMetadata metadata) {
return 1;
}
}
static class BuzzRouter implements MessageRouter {
@Override
public int choosePartition(Message<?> msg, TopicMetadata metadata) {
return 2;
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
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.pulsar.annotation.PulsarListener;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* @author Soby Chacko
*/
public class PulsarListenerTests extends AbstractContainerBaseTest {
static CountDownLatch latch1 = new CountDownLatch(1);
static CountDownLatch latch2 = new CountDownLatch(10);
@Test
void testBasicListener() throws Exception {
SpringApplication app = new SpringApplication(BasicListenerConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext context = app.run("--spring.pulsar.client.serviceUrl=" + getPulsarBrokerUrl())) {
@SuppressWarnings("unchecked")
final PulsarTemplate<String> pulsarTemplate = context.getBean(PulsarTemplate.class);
pulsarTemplate.setDefaultTopicName("hello-pulsar-exclusive");
pulsarTemplate.send("John Doe");
System.out.println("waiting at the latch");
final boolean await = latch1.await(20, TimeUnit.SECONDS);
assertThat(await).isTrue();
}
}
@Test
void testBatchListener() throws Exception {
SpringApplication app = new SpringApplication(BatchListenerConfig.class);
app.setWebApplicationType(WebApplicationType.NONE);
try (ConfigurableApplicationContext context = app.run("--spring.pulsar.client.serviceUrl=" + getPulsarBrokerUrl())) {
@SuppressWarnings("unchecked")
final PulsarTemplate<String> pulsarTemplate = context.getBean(PulsarTemplate.class);
pulsarTemplate.setDefaultTopicName("hello-pulsar-exclusive");
for (int i = 0; i < 10; i++) {
pulsarTemplate.send("John Doe");
}
final boolean await = latch2.await(10, TimeUnit.SECONDS);
assertThat(await).isTrue();
}
}
@EnableAutoConfiguration
public static class BasicListenerConfig {
@PulsarListener(subscriptionName = "test-exclusive-sub-1", topics = "hello-pulsar-exclusive")
public void listen(String foo) {
System.out.println("Message Received from basic: " + foo);
latch1.countDown();
}
}
@EnableAutoConfiguration
public static class BatchListenerConfig {
@PulsarListener(subscriptionName = "test-exclusive-sub-2", topics = "hello-pulsar-exclusive", batch = "true")
public void listen(List<String> foo) {
System.out.println("Message Received from batch: " + foo);
System.out.println("Message Received from batch: " + foo.size());
foo.forEach(t -> latch2.countDown());
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.pulsar.core;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.Schema;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* @author Soby Chacko
*/
public class PulsarTemplateTests extends AbstractContainerBaseTest {
public static final String TEST_TOPIC = "test_topic";
@Test
public void testUsage() throws Exception {
testPulsarFunctionality(getPulsarBrokerUrl());
}
@Test
public void testSendAsync() throws Exception {
Map<String, Object> config = new HashMap<>();
config.put("topicName", "foo-bar-123");
Map<String, Object> clientConfig = new HashMap<>();
clientConfig.put("serviceUrl", getPulsarBrokerUrl());
try (
PulsarClient client = PulsarClient.builder()
.loadConf(clientConfig)
.build();
Consumer<String> consumer = client.newConsumer(Schema.STRING)
.topic("foo-bar-123")
.subscriptionName("xyz-test-subs-123")
.subscribe()
) {
final DefaultPulsarProducerFactory<String> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(client, config);
final PulsarTemplate<String> pulsarTemplate = new PulsarTemplate<>(pulsarProducerFactory);
final CompletableFuture<MessageId> future = pulsarTemplate.sendAsync("hello john doe");
future.thenAccept(m -> System.out.println("Got " + m));
try {
Thread.sleep(2000);
final MessageId messageId = future.get();
System.out.println();
}
catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
CompletableFuture<Message<String>> future0 = consumer.receiveAsync();
Message<String> message = future0.get(5, TimeUnit.SECONDS);
assertThat(new String(message.getData()))
.isEqualTo("hello john doe");
}
}
@Test
public void testSendSync() throws Exception {
Map<String, Object> config = new HashMap<>();
config.put("topicName", "foo-bar-123");
Map<String, Object> clientConfig = new HashMap<>();
clientConfig.put("serviceUrl", getPulsarBrokerUrl());
try (
PulsarClient client = PulsarClient.builder()
.loadConf(clientConfig)
.build();
Consumer<String> consumer = client.newConsumer(Schema.STRING)
.topic("foo-bar-123")
.subscriptionName("xyz-test-subs-123")
.subscribe();
) {
final DefaultPulsarProducerFactory<String> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(client, config);
final PulsarTemplate<String> pulsarTemplate = new PulsarTemplate<>(pulsarProducerFactory);
final MessageId messageId = pulsarTemplate.send("hello john doe");
CompletableFuture<Message<String>> future0 = consumer.receiveAsync();
Message<String> message = future0.get(5, TimeUnit.SECONDS);
assertThat(new String(message.getData()))
.isEqualTo("hello john doe");
}
}
private void testPulsarFunctionality(String pulsarBrokerUrl) throws Exception {
try (
PulsarClient client = PulsarClient.builder()
.serviceUrl(pulsarBrokerUrl)
.build();
Consumer consumer = client.newConsumer()
.topic(TEST_TOPIC)
.subscriptionName("test-subs")
.subscribe();
Producer<byte[]> producer = client.newProducer()
.topic(TEST_TOPIC)
.create()
) {
producer.send("test containers".getBytes());
CompletableFuture<Message> future = consumer.receiveAsync();
Message message = future.get(5, TimeUnit.SECONDS);
assertThat(new String(message.getData()))
.isEqualTo("test containers");
}
}
}