Split reactive into its own module
Remove redundant dep
This commit is contained in:
34
spring-pulsar-reactive/build.gradle
Normal file
34
spring-pulsar-reactive/build.gradle
Normal file
@@ -0,0 +1,34 @@
|
||||
plugins {
|
||||
id 'org.springframework.pulsar.spring-module'
|
||||
}
|
||||
|
||||
description = 'Spring Pulsar Reactive Support'
|
||||
|
||||
dependencies {
|
||||
api project (':spring-pulsar')
|
||||
api 'org.apache.pulsar:pulsar-client-reactive-adapter'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
implementation 'com.google.code.findbugs:jsr305'
|
||||
optional 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8'
|
||||
optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
optional 'com.fasterxml.jackson.datatype:jackson-datatype-joda'
|
||||
optional 'com.jayway.jsonpath:json-path'
|
||||
optional 'io.projectreactor:reactor-core'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
testRuntimeOnly 'ch.qos.logback:logback-classic'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
testImplementation 'org.awaitility:awaitility'
|
||||
testImplementation 'org.hamcrest:hamcrest'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||
testImplementation 'org.springframework:spring-test'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.testcontainers:pulsar'
|
||||
}
|
||||
|
||||
test {
|
||||
testLogging.showStandardStreams = true
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.reactive.aot;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.pulsar.client.admin.internal.OffloadProcessStatusImpl;
|
||||
import org.apache.pulsar.client.admin.internal.PulsarAdminBuilderImpl;
|
||||
import org.apache.pulsar.client.api.Authentication;
|
||||
import org.apache.pulsar.client.api.AuthenticationDataProvider;
|
||||
import org.apache.pulsar.client.impl.conf.ClientConfigurationData;
|
||||
import org.apache.pulsar.client.impl.conf.ConsumerConfigurationData;
|
||||
import org.apache.pulsar.client.impl.conf.ProducerConfigurationData;
|
||||
import org.apache.pulsar.client.util.SecretsSerializer;
|
||||
import org.apache.pulsar.common.protocol.Commands;
|
||||
import org.apache.pulsar.shade.io.netty.buffer.AbstractByteBufAllocator;
|
||||
import org.apache.pulsar.shade.io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import org.apache.pulsar.shade.io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import org.apache.pulsar.shade.io.netty.util.ReferenceCountUtil;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link RuntimeHintsRegistrar} for Spring for Apache Pulsar.
|
||||
*
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
public class ReactivePulsarRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
ReflectionHints reflectionHints = hints.reflection();
|
||||
// The following components need access to declared constructors, invoke declared
|
||||
// methods
|
||||
// and introspect all public methods. The components are a mix of JDK classes,
|
||||
// core Pulsar classes,
|
||||
// some other shaded components available through Pulsar client.
|
||||
Stream.of(HashSet.class, TreeMap.class, Authentication.class, AuthenticationDataProvider.class,
|
||||
SecretsSerializer.class, NioSocketChannel.class, AbstractByteBufAllocator.class,
|
||||
NioDatagramChannel.class, PulsarAdminBuilderImpl.class, OffloadProcessStatusImpl.class, Commands.class,
|
||||
ReferenceCountUtil.class).forEach(
|
||||
type -> reflectionHints.registerType(type,
|
||||
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.INTROSPECT_PUBLIC_METHODS)));
|
||||
|
||||
// In addition to the above member category levels, these components need field
|
||||
// and declared class level access.
|
||||
Stream.of(ClientConfigurationData.class, ConsumerConfigurationData.class, ProducerConfigurationData.class)
|
||||
.forEach(type -> reflectionHints.registerType(type,
|
||||
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INTROSPECT_PUBLIC_METHODS,
|
||||
MemberCategory.DECLARED_CLASSES, MemberCategory.DECLARED_FIELDS)));
|
||||
|
||||
// These are inaccessible interfaces/classes in a normal scenario, thus using the
|
||||
// String version,
|
||||
// and we need field level access in them.
|
||||
Stream.of(
|
||||
"org.apache.pulsar.shade.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields",
|
||||
"org.apache.pulsar.shade.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields",
|
||||
"org.apache.pulsar.shade.io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields",
|
||||
"org.apache.pulsar.shade.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField",
|
||||
"org.apache.pulsar.shade.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField",
|
||||
"org.apache.pulsar.shade.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField")
|
||||
.forEach(typeName -> reflectionHints.registerTypeIfPresent(classLoader, typeName,
|
||||
MemberCategory.DECLARED_FIELDS));
|
||||
|
||||
Stream.of("reactor.core.publisher.Flux", "com.github.benmanes.caffeine.cache.SSMSA",
|
||||
"com.github.benmanes.caffeine.cache.PSAMS")
|
||||
.forEach(typeName -> reflectionHints.registerTypeIfPresent(classLoader, typeName,
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.INTROSPECT_PUBLIC_METHODS));
|
||||
|
||||
// Registering JDK dynamic proxies for these interfaces. Since the Connection
|
||||
// interface is protected,
|
||||
// wee need to use the string version of proxy registration. Although the other
|
||||
// interfaces are public,
|
||||
// due to ConnectionHandler$Connection being protected forces all of them to be
|
||||
// registered using the
|
||||
// string version of the API because all of them need to be registered through a
|
||||
// single call.
|
||||
hints.proxies().registerJdkProxy(TypeReference.of("org.apache.pulsar.shade.io.netty.util.TimerTask"),
|
||||
TypeReference.of("org.apache.pulsar.client.impl.ConnectionHandler$Connection"),
|
||||
TypeReference.of("org.apache.pulsar.client.api.Producer"),
|
||||
TypeReference.of("org.springframework.aop.SpringProxy"),
|
||||
TypeReference.of("org.springframework.aop.framework.Advised"),
|
||||
TypeReference.of("org.springframework.core.DecoratingProxy"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.reactive.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
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.expression.BeanResolver;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageHandler;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageListenerContainer;
|
||||
import org.springframework.pulsar.support.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base implementation for {@link ReactivePulsarListenerEndpoint}.
|
||||
*
|
||||
* @param <T> Message payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public abstract class AbstractReactivePulsarListenerEndpoint<T>
|
||||
implements ReactivePulsarListenerEndpoint<T>, BeanFactoryAware, InitializingBean {
|
||||
|
||||
private String subscriptionName;
|
||||
|
||||
private SubscriptionType subscriptionType;
|
||||
|
||||
private SchemaType schemaType;
|
||||
|
||||
private String id;
|
||||
|
||||
private Collection<String> topics = new ArrayList<>();
|
||||
|
||||
private String topicPattern;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private BeanExpressionResolver resolver;
|
||||
|
||||
private BeanExpressionContext expressionContext;
|
||||
|
||||
private BeanResolver beanResolver;
|
||||
|
||||
private Boolean autoStartup;
|
||||
|
||||
private Boolean fluxListener;
|
||||
|
||||
private Integer concurrency;
|
||||
|
||||
private Boolean useKeyOrderedProcessing;
|
||||
|
||||
@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 && !StringUtils.hasText(getTopicPattern())) {
|
||||
throw new IllegalStateException("Topics or topicPattern 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 = Arrays.asList(topics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTopics() {
|
||||
return new ArrayList<>(this.topics);
|
||||
}
|
||||
|
||||
public void setTopicPattern(String topicPattern) {
|
||||
Assert.notNull(topicPattern, "'topicPattern' must not be null");
|
||||
this.topicPattern = topicPattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopicPattern() {
|
||||
return this.topicPattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Boolean getAutoStartup() {
|
||||
return this.autoStartup;
|
||||
}
|
||||
|
||||
public void setAutoStartup(Boolean autoStartup) {
|
||||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupListenerContainer(ReactivePulsarMessageListenerContainer<T> listenerContainer,
|
||||
@Nullable MessageConverter messageConverter) {
|
||||
|
||||
setupMessageListener(listenerContainer, messageConverter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void setupMessageListener(ReactivePulsarMessageListenerContainer<T> container,
|
||||
@Nullable MessageConverter messageConverter) {
|
||||
|
||||
PulsarMessagingMessageListenerAdapter<T> adapter = createMessageHandler(container, messageConverter);
|
||||
Assert.state(adapter != null, () -> "Endpoint [" + this + "] must provide a non null message handler");
|
||||
container.setupMessageHandler((ReactivePulsarMessageHandler) adapter);
|
||||
}
|
||||
|
||||
protected abstract PulsarMessagingMessageListenerAdapter<T> createMessageHandler(
|
||||
ReactivePulsarMessageListenerContainer<T> container, @Nullable MessageConverter messageConverter);
|
||||
|
||||
@Nullable
|
||||
public Boolean getFluxListener() {
|
||||
return this.fluxListener;
|
||||
}
|
||||
|
||||
public void setFluxListener(boolean fluxListener) {
|
||||
this.fluxListener = fluxListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluxListener() {
|
||||
return this.fluxListener != null && this.fluxListener;
|
||||
}
|
||||
|
||||
public SubscriptionType getSubscriptionType() {
|
||||
return this.subscriptionType;
|
||||
}
|
||||
|
||||
public void setSubscriptionType(SubscriptionType subscriptionType) {
|
||||
this.subscriptionType = subscriptionType;
|
||||
}
|
||||
|
||||
public SchemaType getSchemaType() {
|
||||
return this.schemaType;
|
||||
}
|
||||
|
||||
public void setSchemaType(SchemaType schemaType) {
|
||||
this.schemaType = schemaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Integer getConcurrency() {
|
||||
return this.concurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the concurrency for this endpoint's container.
|
||||
* @param concurrency the concurrency.
|
||||
*/
|
||||
public void setConcurrency(Integer concurrency) {
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getUseKeyOrderedProcessing() {
|
||||
return this.useKeyOrderedProcessing;
|
||||
}
|
||||
|
||||
public void setUseKeyOrderedProcessing(Boolean useKeyOrderedProcessing) {
|
||||
this.useKeyOrderedProcessing = useKeyOrderedProcessing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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.reactive.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.listener.DefaultReactivePulsarMessageListenerContainer;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
|
||||
import org.springframework.pulsar.support.JavaUtils;
|
||||
import org.springframework.pulsar.support.MessageConverter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Concrete implementation for {@link ReactivePulsarListenerContainerFactory}.
|
||||
*
|
||||
* @param <T> Message payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class DefaultReactivePulsarListenerContainerFactory<T> implements ReactivePulsarListenerContainerFactory<T> {
|
||||
|
||||
protected final LogAccessor logger = new LogAccessor(this.getClass());
|
||||
|
||||
private final ReactivePulsarConsumerFactory<T> consumerFactory;
|
||||
|
||||
private final ReactivePulsarContainerProperties<T> containerProperties;
|
||||
|
||||
private Boolean autoStartup;
|
||||
|
||||
private MessageConverter messageConverter;
|
||||
|
||||
private Boolean fluxListener;
|
||||
|
||||
public DefaultReactivePulsarListenerContainerFactory(ReactivePulsarConsumerFactory<T> consumerFactory,
|
||||
ReactivePulsarContainerProperties<T> containerProperties) {
|
||||
this.consumerFactory = consumerFactory;
|
||||
this.containerProperties = containerProperties;
|
||||
}
|
||||
|
||||
protected ReactivePulsarConsumerFactory<T> getConsumerFactory() {
|
||||
return this.consumerFactory;
|
||||
}
|
||||
|
||||
public ReactivePulsarContainerProperties<T> getContainerProperties() {
|
||||
return this.containerProperties;
|
||||
}
|
||||
|
||||
public void setAutoStartup(Boolean autoStartup) {
|
||||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 void setFluxListener(Boolean fluxListener) {
|
||||
this.fluxListener = fluxListener;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public DefaultReactivePulsarMessageListenerContainer<T> createContainerInstance(
|
||||
ReactivePulsarListenerEndpoint<T> endpoint) {
|
||||
|
||||
ReactivePulsarContainerProperties<T> properties = new ReactivePulsarContainerProperties<>();
|
||||
|
||||
if (!CollectionUtils.isEmpty(endpoint.getTopics())) {
|
||||
properties.setTopics(endpoint.getTopics());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(endpoint.getTopicPattern())) {
|
||||
properties.setTopicsPattern(endpoint.getTopicPattern());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(endpoint.getSubscriptionName())) {
|
||||
properties.setSubscriptionName(endpoint.getSubscriptionName());
|
||||
}
|
||||
|
||||
if (endpoint.getSubscriptionType() != null) {
|
||||
properties.setSubscriptionType(endpoint.getSubscriptionType());
|
||||
}
|
||||
else {
|
||||
properties.setSubscriptionType(this.containerProperties.getSubscriptionType());
|
||||
}
|
||||
|
||||
if (endpoint.getSchemaType() != null) {
|
||||
properties.setSchemaType(endpoint.getSchemaType());
|
||||
}
|
||||
else {
|
||||
properties.setSchemaType(this.containerProperties.getSchemaType());
|
||||
}
|
||||
|
||||
if (properties.getSchema() == null) {
|
||||
properties.setSchema((Schema<T>) Schema.BYTES);
|
||||
}
|
||||
|
||||
if (endpoint.getConcurrency() != null) {
|
||||
properties.setConcurrency(endpoint.getConcurrency());
|
||||
}
|
||||
else {
|
||||
properties.setConcurrency(this.containerProperties.getConcurrency());
|
||||
}
|
||||
|
||||
if (endpoint.getUseKeyOrderedProcessing() != null) {
|
||||
properties.setUseKeyOrderedProcessing(endpoint.getUseKeyOrderedProcessing());
|
||||
}
|
||||
else {
|
||||
properties.setUseKeyOrderedProcessing(this.containerProperties.isUseKeyOrderedProcessing());
|
||||
}
|
||||
|
||||
return new DefaultReactivePulsarMessageListenerContainer<>(this.getConsumerFactory(), properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultReactivePulsarMessageListenerContainer<T> createListenerContainer(
|
||||
ReactivePulsarListenerEndpoint<T> endpoint) {
|
||||
DefaultReactivePulsarMessageListenerContainer<T> instance = createContainerInstance(endpoint);
|
||||
if (endpoint instanceof AbstractReactivePulsarListenerEndpoint) {
|
||||
configureEndpoint((AbstractReactivePulsarListenerEndpoint<T>) endpoint);
|
||||
}
|
||||
|
||||
endpoint.setupListenerContainer(instance, this.messageConverter);
|
||||
initializeContainer(instance, endpoint);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void configureEndpoint(AbstractReactivePulsarListenerEndpoint<T> aplEndpoint) {
|
||||
if (aplEndpoint.getFluxListener() == null) {
|
||||
JavaUtils.INSTANCE.acceptIfNotNull(this.fluxListener, aplEndpoint::setFluxListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultReactivePulsarMessageListenerContainer<T> createContainer(String... topics) {
|
||||
ReactivePulsarListenerEndpoint<T> endpoint = new ReactivePulsarListenerEndpointAdapter<>() {
|
||||
|
||||
@Override
|
||||
public List<String> getTopics() {
|
||||
return Arrays.asList(topics);
|
||||
}
|
||||
|
||||
};
|
||||
DefaultReactivePulsarMessageListenerContainer<T> container = createContainerInstance(endpoint);
|
||||
initializeContainer(container, endpoint);
|
||||
return container;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void initializeContainer(DefaultReactivePulsarMessageListenerContainer<T> instance,
|
||||
ReactivePulsarListenerEndpoint<T> endpoint) {
|
||||
Boolean autoStart = endpoint.getAutoStartup();
|
||||
if (autoStart != null) {
|
||||
instance.setAutoStartup(autoStart);
|
||||
}
|
||||
else if (this.autoStartup != null) {
|
||||
instance.setAutoStartup(this.autoStartup);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* 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.reactive.config;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.pulsar.client.api.Consumer;
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.Messages;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.client.impl.schema.AvroSchema;
|
||||
import org.apache.pulsar.client.impl.schema.JSONSchema;
|
||||
import org.apache.pulsar.client.impl.schema.ProtobufSchema;
|
||||
import org.apache.pulsar.common.schema.KeyValueEncodingType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
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.Header;
|
||||
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
import org.springframework.pulsar.core.SchemaUtils;
|
||||
import org.springframework.pulsar.listener.Acknowledgement;
|
||||
import org.springframework.pulsar.listener.adapter.HandlerAdapter;
|
||||
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.listener.DefaultReactivePulsarMessageListenerContainer;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageListenerContainer;
|
||||
import org.springframework.pulsar.reactive.listener.adapter.PulsarReactiveOneByOneMessagingMessageListenerAdapter;
|
||||
import org.springframework.pulsar.reactive.listener.adapter.PulsarReactiveStreamingMessagingMessageListenerAdapter;
|
||||
import org.springframework.pulsar.support.MessageConverter;
|
||||
import org.springframework.pulsar.support.converter.PulsarRecordMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.google.protobuf.GeneratedMessageV3;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* A {@link ReactivePulsarListenerEndpoint} providing the method to invoke to process an
|
||||
* incoming message for this endpoint.
|
||||
*
|
||||
* @param <V> Message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class MethodReactivePulsarListenerEndpoint<V> extends AbstractReactivePulsarListenerEndpoint<V> {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
|
||||
|
||||
private Object bean;
|
||||
|
||||
private Method method;
|
||||
|
||||
private MessageHandlerMethodFactory messageHandlerMethodFactory;
|
||||
|
||||
private SmartMessageConverter messagingConverter;
|
||||
|
||||
private ReactiveMessageConsumerBuilderCustomizer<V> consumerCustomizer;
|
||||
|
||||
private DeadLetterPolicy deadLetterPolicy;
|
||||
|
||||
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
|
||||
@SuppressWarnings("unchecked")
|
||||
protected PulsarMessagingMessageListenerAdapter<V> createMessageHandler(
|
||||
ReactivePulsarMessageListenerContainer<V> container, @Nullable MessageConverter messageConverter) {
|
||||
Assert.state(this.messageHandlerMethodFactory != null,
|
||||
"Could not create message listener - MessageHandlerMethodFactory not set");
|
||||
PulsarMessagingMessageListenerAdapter<V> messageListener = createMessageListenerInstance(messageConverter);
|
||||
HandlerAdapter handlerMethod = configureListenerAdapter(messageListener);
|
||||
messageListener.setHandlerMethod(handlerMethod);
|
||||
|
||||
// Since we have access to the handler method here, check if we can type infer the
|
||||
// Schema used.
|
||||
|
||||
// TODO: filter out the payload type by excluding Consumer, Message, Messages etc.
|
||||
|
||||
MethodParameter[] methodParameters = handlerMethod.getInvokerHandlerMethod().getMethodParameters();
|
||||
MethodParameter messageParameter = null;
|
||||
Optional<MethodParameter> parameter = Arrays.stream(methodParameters)
|
||||
.filter(methodParameter1 -> !methodParameter1.getParameterType().equals(Consumer.class)
|
||||
|| !methodParameter1.getParameterType().equals(Acknowledgement.class)
|
||||
|| !methodParameter1.hasParameterAnnotation(Header.class))
|
||||
.findFirst();
|
||||
long count = Arrays.stream(methodParameters)
|
||||
.filter(methodParameter1 -> !methodParameter1.getParameterType().equals(Consumer.class)
|
||||
&& !methodParameter1.getParameterType().equals(Acknowledgement.class)
|
||||
&& !methodParameter1.hasParameterAnnotation(Header.class))
|
||||
.count();
|
||||
Assert.isTrue(count == 1, "More than 1 expected payload types found");
|
||||
if (parameter.isPresent()) {
|
||||
messageParameter = parameter.get();
|
||||
}
|
||||
|
||||
DefaultReactivePulsarMessageListenerContainer<?> containerInstance = (DefaultReactivePulsarMessageListenerContainer<?>) container;
|
||||
ReactivePulsarContainerProperties<?> pulsarContainerProperties = containerInstance.getContainerProperties();
|
||||
SchemaType schemaType = pulsarContainerProperties.getSchemaType();
|
||||
if (schemaType != SchemaType.NONE) {
|
||||
switch (schemaType) {
|
||||
case STRING -> pulsarContainerProperties.setSchema((Schema) Schema.STRING);
|
||||
case BYTES -> pulsarContainerProperties.setSchema((Schema) Schema.BYTES);
|
||||
case INT8 -> pulsarContainerProperties.setSchema((Schema) Schema.INT8);
|
||||
case INT16 -> pulsarContainerProperties.setSchema((Schema) Schema.INT16);
|
||||
case INT32 -> pulsarContainerProperties.setSchema((Schema) Schema.INT32);
|
||||
case INT64 -> pulsarContainerProperties.setSchema((Schema) Schema.INT64);
|
||||
case BOOLEAN -> pulsarContainerProperties.setSchema((Schema) Schema.BOOL);
|
||||
case DATE -> pulsarContainerProperties.setSchema((Schema) Schema.DATE);
|
||||
case DOUBLE -> pulsarContainerProperties.setSchema((Schema) Schema.DOUBLE);
|
||||
case FLOAT -> pulsarContainerProperties.setSchema((Schema) Schema.FLOAT);
|
||||
case INSTANT -> pulsarContainerProperties.setSchema((Schema) Schema.INSTANT);
|
||||
case LOCAL_DATE -> pulsarContainerProperties.setSchema((Schema) Schema.LOCAL_DATE);
|
||||
case LOCAL_DATE_TIME -> pulsarContainerProperties.setSchema((Schema) Schema.LOCAL_DATE_TIME);
|
||||
case LOCAL_TIME -> pulsarContainerProperties.setSchema((Schema) Schema.LOCAL_TIME);
|
||||
case JSON -> {
|
||||
Schema<?> messageSchema = getMessageSchema(messageParameter, JSONSchema::of);
|
||||
pulsarContainerProperties.setSchema((Schema) messageSchema);
|
||||
}
|
||||
case AVRO -> {
|
||||
Schema<?> messageSchema = getMessageSchema(messageParameter, AvroSchema::of);
|
||||
pulsarContainerProperties.setSchema((Schema) messageSchema);
|
||||
}
|
||||
case PROTOBUF -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Schema<?> messageSchema = getMessageSchema(messageParameter,
|
||||
(c -> ProtobufSchema.of((Class<? extends GeneratedMessageV3>) c)));
|
||||
pulsarContainerProperties.setSchema((Schema) messageSchema);
|
||||
}
|
||||
case KEY_VALUE -> {
|
||||
Schema<?> messageSchema = getMessageKeyValueSchema(messageParameter);
|
||||
pulsarContainerProperties.setSchema((Schema) messageSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (messageParameter != null) {
|
||||
Schema<?> messageSchema = getMessageSchema(messageParameter,
|
||||
(messageClass) -> SchemaUtils.getSchema(messageClass, false));
|
||||
if (messageSchema != null) {
|
||||
pulsarContainerProperties.setSchema((Schema) messageSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
SchemaType type = pulsarContainerProperties.getSchema().getSchemaInfo().getType();
|
||||
pulsarContainerProperties.setSchemaType(type);
|
||||
|
||||
ReactiveMessageConsumerBuilderCustomizer<V> customizer1 = b -> b.deadLetterPolicy(this.deadLetterPolicy);
|
||||
container.setConsumerCustomizer(b -> {
|
||||
if (this.consumerCustomizer != null) {
|
||||
this.consumerCustomizer.customize(b);
|
||||
}
|
||||
customizer1.customize(b);
|
||||
});
|
||||
|
||||
return messageListener;
|
||||
}
|
||||
|
||||
private Schema<?> getMessageSchema(MethodParameter messageParameter, Function<Class<?>, Schema<?>> schemaFactory) {
|
||||
ResolvableType messageType = resolvableType(messageParameter);
|
||||
Class<?> messageClass = messageType.getRawClass();
|
||||
return schemaFactory.apply(messageClass);
|
||||
}
|
||||
|
||||
private Schema<?> getMessageKeyValueSchema(MethodParameter messageParameter) {
|
||||
ResolvableType messageType = resolvableType(messageParameter);
|
||||
Class<?> keyClass = messageType.resolveGeneric(0);
|
||||
Class<?> valueClass = messageType.resolveGeneric(1);
|
||||
Schema<? extends Class<?>> keySchema = SchemaUtils.getSchema(keyClass);
|
||||
Schema<? extends Class<?>> valueSchema = SchemaUtils.getSchema(valueClass);
|
||||
return Schema.KeyValue(keySchema, valueSchema, KeyValueEncodingType.INLINE);
|
||||
}
|
||||
|
||||
private ResolvableType resolvableType(MethodParameter methodParameter) {
|
||||
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
|
||||
Class<?> rawClass = resolvableType.getRawClass();
|
||||
if (rawClass != null && isContainerType(rawClass)) {
|
||||
resolvableType = resolvableType.getGeneric(0);
|
||||
}
|
||||
if (Message.class.isAssignableFrom(resolvableType.getRawClass())
|
||||
|| org.springframework.messaging.Message.class.isAssignableFrom(resolvableType.getRawClass())) {
|
||||
resolvableType = resolvableType.getGeneric(0);
|
||||
}
|
||||
return resolvableType;
|
||||
}
|
||||
|
||||
private boolean isContainerType(Class<?> rawClass) {
|
||||
return rawClass.isAssignableFrom(Flux.class) || rawClass.isAssignableFrom(List.class)
|
||||
|| rawClass.isAssignableFrom(Message.class) || rawClass.isAssignableFrom(Messages.class)
|
||||
|| rawClass.isAssignableFrom(org.springframework.messaging.Message.class);
|
||||
}
|
||||
|
||||
protected HandlerAdapter configureListenerAdapter(PulsarMessagingMessageListenerAdapter<V> messageListener) {
|
||||
InvocableHandlerMethod invocableHandlerMethod = this.messageHandlerMethodFactory
|
||||
.createInvocableHandlerMethod(getBean(), getMethod());
|
||||
return new HandlerAdapter(invocableHandlerMethod);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
protected PulsarMessagingMessageListenerAdapter<V> createMessageListenerInstance(
|
||||
@Nullable MessageConverter messageConverter) {
|
||||
|
||||
PulsarMessagingMessageListenerAdapter<V> listener;
|
||||
if (isFluxListener()) {
|
||||
listener = new PulsarReactiveStreamingMessagingMessageListenerAdapter<V>(this.bean, this.method);
|
||||
}
|
||||
else {
|
||||
listener = new PulsarReactiveOneByOneMessagingMessageListenerAdapter<V>(this.bean, this.method);
|
||||
}
|
||||
|
||||
if (messageConverter instanceof PulsarRecordMessageConverter) {
|
||||
listener.setMessageConverter((PulsarRecordMessageConverter) messageConverter);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public void setDeadLetterPolicy(DeadLetterPolicy deadLetterPolicy) {
|
||||
this.deadLetterPolicy = deadLetterPolicy;
|
||||
}
|
||||
|
||||
public void setConsumerCustomizer(ReactiveMessageConsumerBuilderCustomizer<V> consumerCustomizer) {
|
||||
this.consumerCustomizer = consumerCustomizer;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.reactive.config;
|
||||
|
||||
import org.springframework.pulsar.config.ListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageListenerContainer;
|
||||
|
||||
/**
|
||||
* Factory for Pulsar reactive message listener containers.
|
||||
*
|
||||
* @param <T> Message payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public interface ReactivePulsarListenerContainerFactory<T>
|
||||
extends ListenerContainerFactory<ReactivePulsarMessageListenerContainer<T>, ReactivePulsarListenerEndpoint<T>> {
|
||||
|
||||
}
|
||||
@@ -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.reactive.config;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.config.ListenerEndpoint;
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListenerConfigurationSelector;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageListenerContainer;
|
||||
|
||||
/**
|
||||
* Model for a Pulsar reactive listener endpoint. Can be used against a
|
||||
* {@link ReactivePulsarListenerConfigurationSelector} to register endpoints
|
||||
* programmatically.
|
||||
*
|
||||
* @param <T> Message payload type.
|
||||
* @author Christophe Bornet
|
||||
* @author Chris Bono
|
||||
*/
|
||||
public interface ReactivePulsarListenerEndpoint<T> extends ListenerEndpoint<ReactivePulsarMessageListenerContainer<T>> {
|
||||
|
||||
boolean isFluxListener();
|
||||
|
||||
@Nullable
|
||||
Boolean getUseKeyOrderedProcessing();
|
||||
|
||||
}
|
||||
@@ -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.reactive.config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageListenerContainer;
|
||||
import org.springframework.pulsar.support.MessageConverter;
|
||||
|
||||
/**
|
||||
* Adapter to avoid having to implement all methods.
|
||||
*
|
||||
* @param <T> Message payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class ReactivePulsarListenerEndpointAdapter<T> implements ReactivePulsarListenerEndpoint<T> {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubscriptionName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionType getSubscriptionType() {
|
||||
return SubscriptionType.Exclusive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTopics() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTopicPattern() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getAutoStartup() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupListenerContainer(ReactivePulsarMessageListenerContainer<T> listenerContainer,
|
||||
MessageConverter messageConverter) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchemaType getSchemaType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Integer getConcurrency() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluxListener() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getUseKeyOrderedProcessing() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.reactive.config;
|
||||
|
||||
import org.springframework.pulsar.config.ListenerEndpointRegistry;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageListenerContainer;
|
||||
|
||||
/**
|
||||
* Creates the necessary {@link ReactivePulsarMessageListenerContainer} instances for the
|
||||
* registered {@linkplain ReactivePulsarListenerEndpoint endpoints}. Also manages the
|
||||
* lifecycle of the listener containers, in particular within the lifecycle of the
|
||||
* application context.
|
||||
*
|
||||
* <p>
|
||||
* Contrary to {@link ReactivePulsarMessageListenerContainer}s created manually, listener
|
||||
* containers managed by registry are not beans in the application context and are not
|
||||
* candidates for autowiring. Use {@link #getListenerContainers()} if you need to access
|
||||
* this registry's listener containers for management purposes. If you need to access to a
|
||||
* specific message listener container, use {@link #getListenerContainer(String)} with the
|
||||
* id of the endpoint.
|
||||
*
|
||||
* @param <T> Message payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class ReactivePulsarListenerEndpointRegistry<T>
|
||||
extends ListenerEndpointRegistry<ReactivePulsarMessageListenerContainer<T>, ReactivePulsarListenerEndpoint<T>> {
|
||||
|
||||
public ReactivePulsarListenerEndpointRegistry() {
|
||||
super(ReactivePulsarMessageListenerContainer.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.reactive.config.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;
|
||||
|
||||
/**
|
||||
* Enables detection of {@link ReactivePulsarListener} annotations on any Spring-managed
|
||||
* bean in the container.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import(ReactivePulsarListenerConfigurationSelector.class)
|
||||
public @interface EnableReactivePulsar {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.reactive.config.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.config.PulsarListenerBeanNames;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpointRegistry;
|
||||
|
||||
/**
|
||||
* An {@link ImportBeanDefinitionRegistrar} class that registers a
|
||||
* {@link ReactivePulsarListenerAnnotationBeanPostProcessor} bean capable of processing
|
||||
* Spring's @{@link ReactivePulsarListener} annotation. Also register a default
|
||||
* {@link ReactivePulsarListenerEndpointRegistry}.
|
||||
*
|
||||
* <p>
|
||||
* This configuration class is automatically imported when using
|
||||
* the @{@link EnableReactivePulsar} annotation.
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
* @see ReactivePulsarListenerAnnotationBeanPostProcessor
|
||||
* @see ReactivePulsarListenerEndpointRegistry
|
||||
* @see EnableReactivePulsar
|
||||
*/
|
||||
public class ReactivePulsarBootstrapConfiguration implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
|
||||
if (!registry.containsBeanDefinition(
|
||||
PulsarListenerBeanNames.REACTIVE_PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)) {
|
||||
registry.registerBeanDefinition(
|
||||
PulsarListenerBeanNames.REACTIVE_PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME,
|
||||
new RootBeanDefinition(ReactivePulsarListenerAnnotationBeanPostProcessor.class));
|
||||
}
|
||||
|
||||
if (!registry
|
||||
.containsBeanDefinition(PulsarListenerBeanNames.REACTIVE_PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)) {
|
||||
registry.registerBeanDefinition(
|
||||
PulsarListenerBeanNames.REACTIVE_PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,
|
||||
new RootBeanDefinition(ReactivePulsarListenerEndpointRegistry.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.reactive.config.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.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpointRegistry;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
|
||||
/**
|
||||
* Annotation that marks a method to be the target of a Pulsar message listener on the
|
||||
* specified topics.
|
||||
*
|
||||
* The {@link #containerFactory()} identifies the
|
||||
* {@link ReactivePulsarListenerContainerFactory} 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 @ReactivePulsarListener} annotations is performed by registering a
|
||||
* {@link ReactivePulsarListenerAnnotationBeanPostProcessor}. This can be done manually
|
||||
* or, more conveniently, through {@link EnableReactivePulsar} annotation.
|
||||
* </p>
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MessageMapping
|
||||
@Documented
|
||||
public @interface ReactivePulsarListener {
|
||||
|
||||
/**
|
||||
* 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 placeholders {@code ${...}} are supported.
|
||||
* @return the {@code id} for the container managing for this endpoint.
|
||||
* @see ReactivePulsarListenerEndpointRegistry#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
|
||||
*/
|
||||
SubscriptionType subscriptionType() default SubscriptionType.Exclusive;
|
||||
|
||||
/**
|
||||
* Pulsar schema type for this listener.
|
||||
* @return the {@code schemaType} for this listener
|
||||
*/
|
||||
SchemaType schemaType() default SchemaType.NONE;
|
||||
|
||||
/**
|
||||
* The bean name of the {@link ReactivePulsarListenerContainerFactory} to use to
|
||||
* create the message listener container responsible to serve this endpoint.
|
||||
* <p>
|
||||
* If not specified, the default container factory is used, if any. If a SpEL
|
||||
* expression is provided ({@code #{...}}), the expression can either evaluate to a
|
||||
* container factory instance or a bean name.
|
||||
* @return the container factory bean name.
|
||||
*/
|
||||
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 stream consumption.
|
||||
* @return if true, the listener method shall take a
|
||||
* {@link reactor.core.publisher.Flux} as input argument.
|
||||
*/
|
||||
boolean stream() default false;
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Override the container factory's {@code concurrency} setting for this listener. May
|
||||
* be a property placeholder or SpEL expression that evaluates to a {@link Number}, in
|
||||
* which case {@link Number#intValue()} is used to obtain the value.
|
||||
* <p>
|
||||
* SpEL {@code #{...}} and property placeholders {@code ${...}} are supported.
|
||||
* @return the concurrency.
|
||||
*/
|
||||
String concurrency() 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 keep ordering by message key when concurrency > 1, false to not
|
||||
* keep ordering.
|
||||
*/
|
||||
String useKeyOrderedProcessing() default "";
|
||||
|
||||
/**
|
||||
* The bean name or a 'SpEL' expression that resolves to a
|
||||
* {@link org.apache.pulsar.client.api.DeadLetterPolicy} to use on the consumer to
|
||||
* configure a dead letter policy for message redelivery.
|
||||
* @return the bean name or empty string to not set any dead letter policy.
|
||||
*/
|
||||
String deadLetterPolicy() default "";
|
||||
|
||||
/**
|
||||
* The bean name or a 'SpEL' expression that resolves to a
|
||||
* {@link ReactiveMessageConsumerBuilderCustomizer} to use to configure the consumer.
|
||||
* @return the bean name or empty string to not configure the consumer.
|
||||
*/
|
||||
String consumerCustomizer() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
/*
|
||||
* 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.reactive.config.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 org.apache.commons.logging.LogFactory;
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
|
||||
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.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.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.annotation.PulsarListenerConfigurer;
|
||||
import org.springframework.pulsar.config.PulsarListenerBeanNames;
|
||||
import org.springframework.pulsar.config.PulsarListenerEndpointRegistrar;
|
||||
import org.springframework.pulsar.reactive.config.MethodReactivePulsarListenerEndpoint;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpoint;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpointRegistry;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
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 ReactivePulsarListener} to be invoked by a Pulsar message listener container
|
||||
* created under the covers by a {@link ReactivePulsarListenerContainerFactory} according
|
||||
* to the parameters of the annotation.
|
||||
*
|
||||
* <p>
|
||||
* Annotated methods can use flexible arguments as defined by
|
||||
* {@link ReactivePulsarListener}.
|
||||
*
|
||||
* <p>
|
||||
* This post-processor is automatically registered by the {@link EnableReactivePulsar}
|
||||
* 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 EnableReactivePulsar}
|
||||
* Javadoc for complete usage details.
|
||||
*
|
||||
* @param <V> the payload type.
|
||||
* @author Christophe Bornet
|
||||
* @see ReactivePulsarListener
|
||||
* @see EnableReactivePulsar
|
||||
* @see PulsarListenerConfigurer
|
||||
* @see PulsarListenerEndpointRegistrar
|
||||
* @see ReactivePulsarListenerEndpointRegistry
|
||||
* @see ReactivePulsarListenerEndpoint
|
||||
* @see MethodReactivePulsarListenerEndpoint
|
||||
*/
|
||||
public class ReactivePulsarListenerAnnotationBeanPostProcessor<V>
|
||||
implements BeanPostProcessor, Ordered, ApplicationContextAware, InitializingBean, SmartInitializingSingleton {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
|
||||
|
||||
/**
|
||||
* The bean name of the default {@link ReactivePulsarListenerContainerFactory}.
|
||||
*/
|
||||
public static final String DEFAULT_REACTIVE_PULSAR_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "reactivePulsarListenerContainerFactory";
|
||||
|
||||
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.ReactivePulsarListenerEndpointContainer#";
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private BeanExpressionResolver resolver;
|
||||
|
||||
private BeanExpressionContext expressionContext;
|
||||
|
||||
private ReactivePulsarListenerEndpointRegistry<?> endpointRegistry;
|
||||
|
||||
private String defaultContainerFactoryBeanName = DEFAULT_REACTIVE_PULSAR_LISTENER_CONTAINER_FACTORY_BEAN_NAME;
|
||||
|
||||
private final PulsarListenerEndpointRegistrar registrar = new PulsarListenerEndpointRegistrar(
|
||||
ReactivePulsarListenerContainerFactory.class);
|
||||
|
||||
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(ReactivePulsarListenerEndpointRegistry<?> 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() {
|
||||
buildEnhancer();
|
||||
}
|
||||
|
||||
private void buildEnhancer() {
|
||||
if (this.applicationContext != null) {
|
||||
List<AnnotationEnhancer> enhancers = this.applicationContext
|
||||
.getBeanProvider(AnnotationEnhancer.class, false).orderedStream().toList();
|
||||
if (!enhancers.isEmpty()) {
|
||||
this.enhancer = (attrs, element) -> {
|
||||
for (AnnotationEnhancer enh : enhancers) {
|
||||
attrs = enh.apply(attrs, element);
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
this.registrar.setBeanFactory(this.beanFactory);
|
||||
|
||||
this.beanFactory.getBeanProvider(PulsarListenerConfigurer.class)
|
||||
.forEach(c -> c.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(
|
||||
PulsarListenerBeanNames.REACTIVE_PULSAR_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME,
|
||||
ReactivePulsarListenerEndpointRegistry.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 -
|
||||
// otherwise register default formatters
|
||||
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(Object bean, String beanName) throws BeansException {
|
||||
if (!this.nonAnnotatedClasses.contains(bean.getClass())) {
|
||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||
Map<Method, Set<ReactivePulsarListener>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
|
||||
(MethodIntrospector.MetadataLookup<Set<ReactivePulsarListener>>) method -> {
|
||||
Set<ReactivePulsarListener> 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<ReactivePulsarListener>> entry : annotatedMethods.entrySet()) {
|
||||
Method method = entry.getKey();
|
||||
for (ReactivePulsarListener listener : entry.getValue()) {
|
||||
processReactivePulsarListener(listener, method, bean, beanName);
|
||||
}
|
||||
}
|
||||
this.logger.debug(() -> annotatedMethods.size() + " @ReactivePulsarListener methods processed on bean '"
|
||||
+ beanName + "': " + annotatedMethods);
|
||||
}
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
protected void processReactivePulsarListener(ReactivePulsarListener reactivePulsarListener, Method method,
|
||||
Object bean, String beanName) {
|
||||
Method methodToUse = checkProxy(method, bean);
|
||||
MethodReactivePulsarListenerEndpoint<V> endpoint = new MethodReactivePulsarListenerEndpoint<>();
|
||||
endpoint.setMethod(methodToUse);
|
||||
|
||||
String beanRef = reactivePulsarListener.beanRef();
|
||||
this.listenerScope.addListener(beanRef, bean);
|
||||
String[] topics = resolveTopics(reactivePulsarListener);
|
||||
String topicPattern = getTopicPattern(reactivePulsarListener);
|
||||
processListener(endpoint, reactivePulsarListener, bean, beanName, topics, topicPattern);
|
||||
this.listenerScope.removeListener(beanRef);
|
||||
}
|
||||
|
||||
protected void processListener(MethodReactivePulsarListenerEndpoint<?> endpoint,
|
||||
ReactivePulsarListener ReactivePulsarListener, Object bean, String beanName, String[] topics,
|
||||
String topicPattern) {
|
||||
|
||||
processReactivePulsarListenerAnnotation(endpoint, ReactivePulsarListener, bean, topics, topicPattern);
|
||||
|
||||
String containerFactory = resolve(ReactivePulsarListener.containerFactory());
|
||||
ReactivePulsarListenerContainerFactory<?> listenerContainerFactory = resolveContainerFactory(
|
||||
ReactivePulsarListener, containerFactory, beanName);
|
||||
|
||||
this.registrar.registerEndpoint(endpoint, listenerContainerFactory);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ReactivePulsarListenerContainerFactory<?> resolveContainerFactory(
|
||||
ReactivePulsarListener ReactivePulsarListener, Object factoryTarget, String beanName) {
|
||||
|
||||
String containerFactory = ReactivePulsarListener.containerFactory();
|
||||
if (!StringUtils.hasText(containerFactory)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ReactivePulsarListenerContainerFactory<?> factory = null;
|
||||
|
||||
Object resolved = resolveExpression(containerFactory);
|
||||
if (resolved instanceof ReactivePulsarListenerContainerFactory) {
|
||||
return (ReactivePulsarListenerContainerFactory<?>) resolved;
|
||||
}
|
||||
String containerFactoryBeanName = resolveExpressionAsString(containerFactory, "containerFactory");
|
||||
if (StringUtils.hasText(containerFactoryBeanName)) {
|
||||
assertBeanFactory();
|
||||
try {
|
||||
factory = this.beanFactory.getBean(containerFactoryBeanName,
|
||||
ReactivePulsarListenerContainerFactory.class);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
throw new BeanInitializationException(noBeanFoundMessage(factoryTarget, beanName,
|
||||
containerFactoryBeanName, ReactivePulsarListenerContainerFactory.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 processReactivePulsarListenerAnnotation(MethodReactivePulsarListenerEndpoint<?> endpoint,
|
||||
ReactivePulsarListener reactivePulsarListener, Object bean, String[] topics, String topicPattern) {
|
||||
|
||||
endpoint.setBean(bean);
|
||||
endpoint.setMessageHandlerMethodFactory(this.messageHandlerMethodFactory);
|
||||
endpoint.setSubscriptionName(getEndpointSubscriptionName(reactivePulsarListener));
|
||||
endpoint.setId(getEndpointId(reactivePulsarListener));
|
||||
endpoint.setTopics(topics);
|
||||
endpoint.setTopicPattern(topicPattern);
|
||||
endpoint.setSubscriptionType(reactivePulsarListener.subscriptionType());
|
||||
endpoint.setSchemaType(reactivePulsarListener.schemaType());
|
||||
|
||||
String concurrency = reactivePulsarListener.concurrency();
|
||||
if (StringUtils.hasText(concurrency)) {
|
||||
endpoint.setConcurrency(resolveExpressionAsInteger(concurrency, "concurrency"));
|
||||
}
|
||||
String useKeyOrderedProcessing = reactivePulsarListener.useKeyOrderedProcessing();
|
||||
if (StringUtils.hasText(useKeyOrderedProcessing)) {
|
||||
endpoint.setUseKeyOrderedProcessing(
|
||||
resolveExpressionAsBoolean(useKeyOrderedProcessing, "useKeyOrderedProcessing"));
|
||||
}
|
||||
|
||||
String autoStartup = reactivePulsarListener.autoStartup();
|
||||
if (StringUtils.hasText(autoStartup)) {
|
||||
endpoint.setAutoStartup(resolveExpressionAsBoolean(autoStartup, "autoStartup"));
|
||||
}
|
||||
endpoint.setFluxListener(reactivePulsarListener.stream());
|
||||
endpoint.setBeanFactory(this.beanFactory);
|
||||
|
||||
resolveDeadLetterPolicy(endpoint, reactivePulsarListener);
|
||||
resolveConsumerCustomizer(endpoint, reactivePulsarListener);
|
||||
}
|
||||
|
||||
private void resolveDeadLetterPolicy(MethodReactivePulsarListenerEndpoint<?> endpoint,
|
||||
ReactivePulsarListener reactivePulsarListener) {
|
||||
Object deadLetterPolicy = resolveExpression(reactivePulsarListener.deadLetterPolicy());
|
||||
if (deadLetterPolicy instanceof DeadLetterPolicy) {
|
||||
endpoint.setDeadLetterPolicy((DeadLetterPolicy) deadLetterPolicy);
|
||||
}
|
||||
else {
|
||||
String deadLetterPolicyBeanName = resolveExpressionAsString(reactivePulsarListener.deadLetterPolicy(),
|
||||
"deadLetterPolicy");
|
||||
if (StringUtils.hasText(deadLetterPolicyBeanName)) {
|
||||
endpoint.setDeadLetterPolicy(
|
||||
this.beanFactory.getBean(deadLetterPolicyBeanName, DeadLetterPolicy.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void resolveConsumerCustomizer(MethodReactivePulsarListenerEndpoint<?> endpoint,
|
||||
ReactivePulsarListener reactivePulsarListener) {
|
||||
Object customizer = resolveExpression(reactivePulsarListener.consumerCustomizer());
|
||||
if (customizer instanceof ReactiveMessageConsumerBuilderCustomizer<?>) {
|
||||
endpoint.setConsumerCustomizer((ReactiveMessageConsumerBuilderCustomizer) customizer);
|
||||
}
|
||||
else {
|
||||
String consumerCustomizerBeanName = resolveExpressionAsString(reactivePulsarListener.consumerCustomizer(),
|
||||
"consumerCustomizer");
|
||||
if (StringUtils.hasText(consumerCustomizerBeanName)) {
|
||||
endpoint.setConsumerCustomizer(this.beanFactory.getBean(consumerCustomizerBeanName,
|
||||
ReactiveMessageConsumerBuilderCustomizer.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Integer resolveExpressionAsInteger(String value, String attribute) {
|
||||
Object resolved = resolveExpression(value);
|
||||
Integer result = null;
|
||||
if (resolved instanceof String) {
|
||||
result = Integer.parseInt((String) resolved);
|
||||
}
|
||||
else if (resolved instanceof Number) {
|
||||
result = ((Number) resolved).intValue();
|
||||
}
|
||||
else if (resolved != null) {
|
||||
throw new IllegalStateException(
|
||||
THE_LEFT + attribute + "] must resolve to an Number or a String that can be parsed as an Integer. "
|
||||
+ RESOLVED_TO_LEFT + resolved.getClass() + RIGHT_FOR_LEFT + value + "]");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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 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(ReactivePulsarListener reactivePulsarListener) {
|
||||
if (StringUtils.hasText(reactivePulsarListener.subscriptionName())) {
|
||||
return resolveExpressionAsString(reactivePulsarListener.subscriptionName(), "subscriptionName");
|
||||
}
|
||||
return GENERATED_ID_PREFIX + this.counter.getAndIncrement();
|
||||
}
|
||||
|
||||
private String getEndpointId(ReactivePulsarListener reactivePulsarListener) {
|
||||
if (StringUtils.hasText(reactivePulsarListener.id())) {
|
||||
return resolveExpressionAsString(reactivePulsarListener.id(), "id");
|
||||
}
|
||||
return GENERATED_ID_PREFIX + this.counter.getAndIncrement();
|
||||
}
|
||||
|
||||
private String getTopicPattern(ReactivePulsarListener reactivePulsarListener) {
|
||||
return resolveExpressionAsString(reactivePulsarListener.topicPattern(), "topicPattern");
|
||||
}
|
||||
|
||||
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(ReactivePulsarListener ReactivePulsarListener) {
|
||||
String[] topics = ReactivePulsarListener.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;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
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("@ReactivePulsarListener 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 @ReactivePulsarListener 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(
|
||||
"@ReactivePulsarListener 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<ReactivePulsarListener> findListenerAnnotations(Class<?> clazz) {
|
||||
Set<ReactivePulsarListener> listeners = new HashSet<>();
|
||||
ReactivePulsarListener ann = AnnotatedElementUtils.findMergedAnnotation(clazz, ReactivePulsarListener.class);
|
||||
if (ann != null) {
|
||||
ann = enhance(clazz, ann);
|
||||
listeners.add(ann);
|
||||
}
|
||||
ReactivePulsarListeners anns = AnnotationUtils.findAnnotation(clazz, ReactivePulsarListeners.class);
|
||||
if (anns != null) {
|
||||
listeners.addAll(Arrays.stream(anns.value()).map(anno -> enhance(clazz, anno)).toList());
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
private Set<ReactivePulsarListener> findListenerAnnotations(Method method) {
|
||||
Set<ReactivePulsarListener> listeners = new HashSet<>();
|
||||
ReactivePulsarListener ann = AnnotatedElementUtils.findMergedAnnotation(method, ReactivePulsarListener.class);
|
||||
if (ann != null) {
|
||||
ann = enhance(method, ann);
|
||||
listeners.add(ann);
|
||||
}
|
||||
ReactivePulsarListeners anns = AnnotationUtils.findAnnotation(method, ReactivePulsarListeners.class);
|
||||
if (anns != null) {
|
||||
listeners.addAll(Arrays.stream(anns.value()).map(anno -> enhance(method, anno)).toList());
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
private ReactivePulsarListener enhance(AnnotatedElement element, ReactivePulsarListener ann) {
|
||||
if (this.enhancer == null) {
|
||||
return ann;
|
||||
}
|
||||
return AnnotationUtils.synthesizeAnnotation(
|
||||
this.enhancer.apply(AnnotationUtils.getAnnotationAttributes(ann), element),
|
||||
ReactivePulsarListener.class, null);
|
||||
}
|
||||
|
||||
private void addFormatters(FormatterRegistry registry) {
|
||||
this.beanFactory.getBeanProvider(Converter.class).forEach(registry::addConverter);
|
||||
this.beanFactory.getBeanProvider(GenericConverter.class).forEach(registry::addConverter);
|
||||
this.beanFactory.getBeanProvider(Formatter.class).forEach(registry::addFormatter);
|
||||
}
|
||||
|
||||
@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 = ReactivePulsarListenerAnnotationBeanPostProcessor.this.registrar.getValidator();
|
||||
if (validator != null) {
|
||||
defaultFactory.setValidator(validator);
|
||||
}
|
||||
defaultFactory.setBeanFactory(ReactivePulsarListenerAnnotationBeanPostProcessor.this.beanFactory);
|
||||
this.defaultFormattingConversionService.addConverter(
|
||||
new BytesToStringConverter(ReactivePulsarListenerAnnotationBeanPostProcessor.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<>(
|
||||
ReactivePulsarListenerAnnotationBeanPostProcessor.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);
|
||||
}
|
||||
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>> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.reactive.config.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
|
||||
* {@link ReactivePulsarBootstrapConfiguration} as late as possible.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
@Order
|
||||
public class ReactivePulsarListenerConfigurationSelector implements DeferredImportSelector {
|
||||
|
||||
@Override
|
||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
|
||||
return new String[] { ReactivePulsarBootstrapConfiguration.class.getName() };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.reactive.config.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;
|
||||
|
||||
/**
|
||||
* Container annotation that aggregates several {@link ReactivePulsarListener}
|
||||
* annotations.
|
||||
* <p>
|
||||
* Can be used natively, declaring several nested {@link ReactivePulsarListener}
|
||||
* annotations. Can also be used in conjunction with Java 8's support for repeatable
|
||||
* annotations, where {@link ReactivePulsarListener} can simply be declared several times
|
||||
* on the same method (or class), implicitly generating this container annotation.
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*
|
||||
* @see ReactivePulsarListener
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ReactivePulsarListeners {
|
||||
|
||||
ReactivePulsarListener[] value();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Package containing annotations used by the framework.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.pulsar.reactive.config.annotation;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Package containing Spring configuration classes for the framework.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.pulsar.reactive.config;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.ImmutableReactiveMessageConsumerSpec;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageConsumerSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumer;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Default implementation for {@link ReactivePulsarConsumerFactory}.
|
||||
*
|
||||
* @param <T> underlying payload type for the reactive consumer.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class DefaultReactivePulsarConsumerFactory<T> implements ReactivePulsarConsumerFactory<T> {
|
||||
|
||||
private final ReactiveMessageConsumerSpec consumerSpec;
|
||||
|
||||
private final ReactivePulsarClient reactivePulsarClient;
|
||||
|
||||
public DefaultReactivePulsarConsumerFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
ReactiveMessageConsumerSpec consumerSpec) {
|
||||
this.consumerSpec = new ImmutableReactiveMessageConsumerSpec(
|
||||
consumerSpec != null ? consumerSpec : new MutableReactiveMessageConsumerSpec());
|
||||
this.reactivePulsarClient = reactivePulsarClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageConsumer<T> createConsumer(Schema<T> schema) {
|
||||
return createConsumer(schema, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageConsumer<T> createConsumer(Schema<T> schema,
|
||||
List<ReactiveMessageConsumerBuilderCustomizer<T>> customizers) {
|
||||
|
||||
ReactiveMessageConsumerBuilder<T> consumer = this.reactivePulsarClient.messageConsumer(schema);
|
||||
|
||||
consumer.applySpec(this.consumerSpec);
|
||||
if (!CollectionUtils.isEmpty(customizers)) {
|
||||
customizers.forEach((c) -> c.customize(consumer));
|
||||
}
|
||||
|
||||
return consumer.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReader;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Default implementation for {@link ReactivePulsarReaderFactory}.
|
||||
*
|
||||
* @param <T> underlying payload type for the reactive reader.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class DefaultReactivePulsarReaderFactory<T> implements ReactivePulsarReaderFactory<T> {
|
||||
|
||||
private final ReactiveMessageReaderSpec readerSpec;
|
||||
|
||||
private final ReactivePulsarClient reactivePulsarClient;
|
||||
|
||||
public DefaultReactivePulsarReaderFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
ReactiveMessageReaderSpec readerSpec) {
|
||||
this.reactivePulsarClient = reactivePulsarClient;
|
||||
this.readerSpec = readerSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageReader<T> createReader(Schema<T> schema) {
|
||||
return createReader(schema, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageReader<T> createReader(Schema<T> schema,
|
||||
List<ReactiveMessageReaderBuilderCustomizer<T>> customizers) {
|
||||
|
||||
ReactiveMessageReaderBuilder<T> reader = this.reactivePulsarClient.messageReader(schema)
|
||||
.applySpec(this.readerSpec);
|
||||
if (!CollectionUtils.isEmpty(customizers)) {
|
||||
customizers.forEach((c) -> c.customize(reader));
|
||||
}
|
||||
return reader.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.api.ImmutableReactiveMessageSenderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageSenderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSender;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ReactivePulsarSenderFactory}.
|
||||
*
|
||||
* @param <T> reactive sender type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class DefaultReactivePulsarSenderFactory<T> implements ReactivePulsarSenderFactory<T> {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(this.getClass());
|
||||
|
||||
private final ReactivePulsarClient reactivePulsarClient;
|
||||
|
||||
private final ReactiveMessageSenderSpec reactiveMessageSenderSpec;
|
||||
|
||||
private final ReactiveMessageSenderCache reactiveMessageSenderCache;
|
||||
|
||||
public DefaultReactivePulsarSenderFactory(PulsarClient pulsarClient,
|
||||
ReactiveMessageSenderSpec reactiveMessageSenderSpec,
|
||||
ReactiveMessageSenderCache reactiveMessageSenderCache) {
|
||||
this(AdaptedReactivePulsarClientFactory.create(pulsarClient), reactiveMessageSenderSpec,
|
||||
reactiveMessageSenderCache);
|
||||
}
|
||||
|
||||
public DefaultReactivePulsarSenderFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
ReactiveMessageSenderSpec reactiveMessageSenderSpec,
|
||||
ReactiveMessageSenderCache reactiveMessageSenderCache) {
|
||||
this.reactivePulsarClient = reactivePulsarClient;
|
||||
this.reactiveMessageSenderSpec = new ImmutableReactiveMessageSenderSpec(
|
||||
reactiveMessageSenderSpec != null ? reactiveMessageSenderSpec : new MutableReactiveMessageSenderSpec());
|
||||
this.reactiveMessageSenderCache = reactiveMessageSenderCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageSender<T> createSender(String topic, Schema<T> schema) {
|
||||
return doCreateReactiveMessageSender(topic, schema, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageSender<T> createSender(String topic, Schema<T> schema,
|
||||
List<ReactiveMessageSenderBuilderCustomizer<T>> customizers) {
|
||||
return doCreateReactiveMessageSender(topic, schema, customizers);
|
||||
}
|
||||
|
||||
private ReactiveMessageSender<T> doCreateReactiveMessageSender(String topic, Schema<T> schema,
|
||||
List<ReactiveMessageSenderBuilderCustomizer<T>> customizers) {
|
||||
String resolvedTopic = ReactiveMessageSenderUtils.resolveTopicName(topic, this);
|
||||
this.logger.trace(() -> String.format("Creating reactive message sender for '%s' topic", resolvedTopic));
|
||||
ReactiveMessageSenderBuilder<T> sender = this.reactivePulsarClient.messageSender(schema);
|
||||
sender.applySpec(this.reactiveMessageSenderSpec);
|
||||
sender.topic(resolvedTopic);
|
||||
if (this.reactiveMessageSenderCache != null) {
|
||||
sender.cache(this.reactiveMessageSenderCache);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(customizers)) {
|
||||
customizers.forEach((c) -> c.customize(sender));
|
||||
}
|
||||
return sender.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveMessageSenderSpec getReactiveMessageSenderSpec() {
|
||||
return this.reactiveMessageSenderSpec;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.reactive.core;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.MessageSpecBuilder;
|
||||
|
||||
/**
|
||||
* The interface to customize a {@link MessageSpecBuilder}.
|
||||
*
|
||||
* @param <T> The message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MessageSpecBuilderCustomizer<T> {
|
||||
|
||||
/**
|
||||
* Customizes a {@link MessageSpecBuilder}.
|
||||
* @param messageSpecBuilder the MessageSpecBuilder to customize
|
||||
*/
|
||||
void customize(MessageSpecBuilder<T> messageSpecBuilder);
|
||||
|
||||
}
|
||||
@@ -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.reactive.core;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
|
||||
/**
|
||||
* The interface to customize a {@link ReactiveMessageConsumerBuilder}.
|
||||
*
|
||||
* @param <T> The message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveMessageConsumerBuilderCustomizer<T> {
|
||||
|
||||
/**
|
||||
* Customizes a {@link ReactiveMessageConsumerBuilder}.
|
||||
* @param reactiveMessageConsumerBuilder the builder to customize
|
||||
*/
|
||||
void customize(ReactiveMessageConsumerBuilder<T> reactiveMessageConsumerBuilder);
|
||||
|
||||
}
|
||||
@@ -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.reactive.core;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
|
||||
/**
|
||||
* The interface to customize a {@link ReactiveMessageReaderBuilder}.
|
||||
*
|
||||
* @param <T> The message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveMessageReaderBuilderCustomizer<T> {
|
||||
|
||||
/**
|
||||
* Customizes a {@link ReactiveMessageReaderBuilder}.
|
||||
* @param reactiveMessageReaderBuilder the builder to customize
|
||||
*/
|
||||
void customize(ReactiveMessageReaderBuilder<T> reactiveMessageReaderBuilder);
|
||||
|
||||
}
|
||||
@@ -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.reactive.core;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder;
|
||||
|
||||
/**
|
||||
* The interface to customize a {@link ReactiveMessageSenderBuilder}.
|
||||
*
|
||||
* @param <T> The message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveMessageSenderBuilderCustomizer<T> {
|
||||
|
||||
/**
|
||||
* Customizes a {@link ReactiveMessageSenderBuilder}.
|
||||
* @param reactiveMessageSenderBuilder the builder to customize
|
||||
*/
|
||||
void customize(ReactiveMessageSenderBuilder<T> reactiveMessageSenderBuilder);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderSpec;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Common utilities used by reactive sender components.
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
final class ReactiveMessageSenderUtils {
|
||||
|
||||
private ReactiveMessageSenderUtils() {
|
||||
}
|
||||
|
||||
static <T> String resolveTopicName(String userSpecifiedTopic,
|
||||
ReactivePulsarSenderFactory<T> reactiveMessageSenderFactory) {
|
||||
ReactiveMessageSenderSpec reactiveMessageSenderSpec = reactiveMessageSenderFactory
|
||||
.getReactiveMessageSenderSpec();
|
||||
if (StringUtils.hasText(userSpecifiedTopic)) {
|
||||
return userSpecifiedTopic;
|
||||
}
|
||||
return Optional.ofNullable(reactiveMessageSenderSpec).map(ReactiveMessageSenderSpec::getTopicName).orElseThrow(
|
||||
() -> new IllegalArgumentException("Topic must be specified when no default topic is configured"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumer;
|
||||
|
||||
/**
|
||||
* Pulsar reactive consumer factory interface.
|
||||
*
|
||||
* @param <T> payload type for the consumer.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public interface ReactivePulsarConsumerFactory<T> {
|
||||
|
||||
/**
|
||||
* Create a reactive message consumer.
|
||||
* @param schema the schema of the messages to be consumed
|
||||
* @return the reactive message consumer
|
||||
*/
|
||||
ReactiveMessageConsumer<T> createConsumer(Schema<T> schema);
|
||||
|
||||
/**
|
||||
* Create a reactive message consumer.
|
||||
* @param schema the schema of the messages to be consumed
|
||||
* @param customizers the optional list of customizers to apply to the reactive
|
||||
* message consumer builder
|
||||
* @return the reactive message consumer
|
||||
*/
|
||||
ReactiveMessageConsumer<T> createConsumer(Schema<T> schema,
|
||||
List<ReactiveMessageConsumerBuilderCustomizer<T>> customizers);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import org.apache.pulsar.client.api.MessageId;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* The Pulsar reactive send operations contract.
|
||||
*
|
||||
* @param <T> the message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public interface ReactivePulsarOperations<T> {
|
||||
|
||||
/**
|
||||
* Sends a message to the default topic in a reactive manner.
|
||||
* @param message the message to send
|
||||
* @return the id assigned by the broker to the published message
|
||||
*/
|
||||
Mono<MessageId> send(T message);
|
||||
|
||||
/**
|
||||
* Sends a message to the specified topic in a reactive manner.
|
||||
* @param topic the topic to send the message to or {@code null} to send to the
|
||||
* default topic
|
||||
* @param message the message to send
|
||||
* @return the id assigned by the broker to the published message
|
||||
*/
|
||||
Mono<MessageId> send(String topic, T message);
|
||||
|
||||
/**
|
||||
* Sends multiple messages to the default topic in a reactive manner.
|
||||
* @param messages the messages to send
|
||||
* @return the ids assigned by the broker to the published messages in the same order
|
||||
* as they were sent
|
||||
*/
|
||||
Flux<MessageId> send(Publisher<T> messages);
|
||||
|
||||
/**
|
||||
* Sends multiple messages to the specified topic in a reactive manner.
|
||||
* @param topic the topic to send the message to or {@code null} to send to the
|
||||
* default topic
|
||||
* @param messages the messages to send
|
||||
* @return the ids assigned by the broker to the published messages in the same order
|
||||
* as they were sent
|
||||
*/
|
||||
Flux<MessageId> send(String topic, Publisher<T> messages);
|
||||
|
||||
/**
|
||||
* Create a {@link SendMessageBuilder builder} for configuring and sending a message
|
||||
* reactively.
|
||||
* @param message the payload of the message
|
||||
* @return the builder to configure and send the message
|
||||
*/
|
||||
SendMessageBuilder<T> newMessage(T message);
|
||||
|
||||
/**
|
||||
* Builder that can be used to configure and send a message. Provides more options
|
||||
* than the send methods provided by {@link ReactivePulsarOperations}.
|
||||
*
|
||||
* @param <T> the message payload type
|
||||
*/
|
||||
interface SendMessageBuilder<T> {
|
||||
|
||||
/**
|
||||
* Specify the topic to send the message to.
|
||||
* @param topic the destination topic
|
||||
* @return the current builder with the destination topic specified
|
||||
*/
|
||||
SendMessageBuilder<T> withTopic(String topic);
|
||||
|
||||
/**
|
||||
* Specifies the message customizer to use to further configure the message.
|
||||
* @param customizer the message customizer
|
||||
* @return the current builder with the message customizer specified
|
||||
*/
|
||||
SendMessageBuilder<T> withMessageCustomizer(MessageSpecBuilderCustomizer<T> customizer);
|
||||
|
||||
/**
|
||||
* Specifies the customizer to use to further configure the reactive sender
|
||||
* builder.
|
||||
* @param customizer the reactive sender builder customizer
|
||||
* @return the current builder with the reactive sender builder customizer
|
||||
* specified
|
||||
*/
|
||||
SendMessageBuilder<T> withSenderCustomizer(ReactiveMessageSenderBuilderCustomizer<T> customizer);
|
||||
|
||||
/**
|
||||
* Send the message in a reactive manner using the configured specification.
|
||||
* @return the id assigned by the broker to the published message
|
||||
*/
|
||||
Mono<MessageId> send();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReader;
|
||||
|
||||
/**
|
||||
* The strategy to create a {@link ReactiveMessageReader} instance(s).
|
||||
*
|
||||
* @param <T> reactive message reader payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public interface ReactivePulsarReaderFactory<T> {
|
||||
|
||||
/**
|
||||
* Create a reactive message reader.
|
||||
* @param schema the schema of the messages to be read
|
||||
* @return the reactive message reader
|
||||
*/
|
||||
ReactiveMessageReader<T> createReader(Schema<T> schema);
|
||||
|
||||
/**
|
||||
* Create a reactive message reader.
|
||||
* @param schema the schema of the messages to be read
|
||||
* @param customizers the optional list of readers to apply to the reactive message
|
||||
* reader builder
|
||||
* @return the reactive message reader
|
||||
*/
|
||||
ReactiveMessageReader<T> createReader(Schema<T> schema,
|
||||
List<ReactiveMessageReaderBuilderCustomizer<T>> customizers);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSender;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderSpec;
|
||||
|
||||
/**
|
||||
* The strategy to create a {@link ReactiveMessageSender} instance(s).
|
||||
*
|
||||
* @param <T> reactive message sender payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public interface ReactivePulsarSenderFactory<T> {
|
||||
|
||||
/**
|
||||
* Create a reactive message sender.
|
||||
* @param topic the topic the reactive message sender will send messages to or
|
||||
* {@code null} to use the default topic
|
||||
* @param schema the schema of the messages to be sent
|
||||
* @return the reactive message sender
|
||||
*/
|
||||
ReactiveMessageSender<T> createSender(String topic, Schema<T> schema);
|
||||
|
||||
/**
|
||||
* Create a reactive message sender.
|
||||
* @param topic the topic the reactive message sender will send messages to or
|
||||
* {@code null} to use the default topic
|
||||
* @param schema the schema of the messages to be sent
|
||||
* @param customizers the optional list of customizers to apply to the reactive
|
||||
* message sender builder
|
||||
* @return the reactive message sender
|
||||
*/
|
||||
ReactiveMessageSender<T> createSender(String topic, Schema<T> schema,
|
||||
List<ReactiveMessageSenderBuilderCustomizer<T>> customizers);
|
||||
|
||||
/**
|
||||
* Return the ReactiveMessageSenderSpec to use when creating reactive senders.
|
||||
* @return the ReactiveMessageSenderSpec
|
||||
*/
|
||||
ReactiveMessageSenderSpec getReactiveMessageSenderSpec();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.pulsar.client.api.MessageId;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.MessageSpec;
|
||||
import org.apache.pulsar.reactive.client.api.MessageSpecBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSender;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.pulsar.core.SchemaUtils;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* A thread-safe template for executing high-level reactive Pulsar operations.
|
||||
*
|
||||
* @param <T> the message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class ReactivePulsarTemplate<T> implements ReactivePulsarOperations<T> {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(this.getClass());
|
||||
|
||||
private final ReactivePulsarSenderFactory<T> reactiveMessageSenderFactory;
|
||||
|
||||
private Schema<T> schema;
|
||||
|
||||
/**
|
||||
* Construct a template instance with observation configuration.
|
||||
* @param reactiveMessageSenderFactory the factory used to create the backing Pulsar
|
||||
* reactive senders
|
||||
*/
|
||||
public ReactivePulsarTemplate(ReactivePulsarSenderFactory<T> reactiveMessageSenderFactory) {
|
||||
this.reactiveMessageSenderFactory = reactiveMessageSenderFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MessageId> send(T message) {
|
||||
return send(null, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MessageId> send(String topic, T message) {
|
||||
return doSend(topic, message, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<MessageId> send(Publisher<T> messages) {
|
||||
return send(null, messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<MessageId> send(String topic, Publisher<T> messages) {
|
||||
return doSendMany(topic, Flux.from(messages));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageBuilderImpl<T> newMessage(T message) {
|
||||
return new SendMessageBuilderImpl<>(this, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the schema to use on this template.
|
||||
* @param schema provides the {@link Schema} used on this template
|
||||
*/
|
||||
public void setSchema(Schema<T> schema) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
private Mono<MessageId> doSend(String topic, T message,
|
||||
MessageSpecBuilderCustomizer<T> messageSpecBuilderCustomizer,
|
||||
ReactiveMessageSenderBuilderCustomizer<T> customizer) {
|
||||
String topicName = ReactiveMessageSenderUtils.resolveTopicName(topic, this.reactiveMessageSenderFactory);
|
||||
this.logger.trace(() -> String.format("Sending reactive message to '%s' topic", topicName));
|
||||
ReactiveMessageSender<T> sender = createMessageSender(topic, message, customizer);
|
||||
return sender.sendOne(getMessageSpec(messageSpecBuilderCustomizer, message)).doOnError(
|
||||
ex -> this.logger.error(ex, () -> String.format("Failed to send message to '%s' topic", topicName)))
|
||||
.doOnSuccess(msgId -> this.logger.trace(() -> String.format("Sent message to '%s' topic", topicName)));
|
||||
|
||||
}
|
||||
|
||||
private Flux<MessageId> doSendMany(String topic, Flux<T> messages) {
|
||||
String topicName = ReactiveMessageSenderUtils.resolveTopicName(topic, this.reactiveMessageSenderFactory);
|
||||
this.logger.trace(() -> String.format("Sending reactive messages to '%s' topic", topicName));
|
||||
|
||||
if (this.schema != null) {
|
||||
/*
|
||||
* If the template has a schema, we can create the message sender right away
|
||||
* and use ReactiveMessageSender::sendMany to send them as a stream. Otherwise
|
||||
* we need to wait to get a message to create it and we can't share it between
|
||||
* messages. So we create one each time and use ReactiveMessageSender::sendOne
|
||||
* to send messages individually.
|
||||
*/
|
||||
ReactiveMessageSender<T> sender = createMessageSender(topic, null, null);
|
||||
return messages.map(MessageSpec::of).as(sender::sendMany)
|
||||
.doOnError(ex -> this.logger.error(ex,
|
||||
() -> String.format("Failed to send messages to '%s' topic", topicName)))
|
||||
.doOnNext(
|
||||
msgId -> this.logger.trace(() -> String.format("Sent messages to '%s' topic", topicName)));
|
||||
}
|
||||
return messages.flatMapSequential(message -> doSend(topic, message, null, null));
|
||||
}
|
||||
|
||||
private static <T> MessageSpec<T> getMessageSpec(MessageSpecBuilderCustomizer<T> messageSpecBuilderCustomizer,
|
||||
T message) {
|
||||
MessageSpecBuilder<T> messageSpecBuilder = MessageSpec.builder(message);
|
||||
|
||||
if (messageSpecBuilderCustomizer != null) {
|
||||
messageSpecBuilderCustomizer.customize(messageSpecBuilder);
|
||||
}
|
||||
|
||||
return messageSpecBuilder.build();
|
||||
}
|
||||
|
||||
private ReactiveMessageSender<T> createMessageSender(String topic, T message,
|
||||
ReactiveMessageSenderBuilderCustomizer<T> customizer) {
|
||||
Schema<T> schema = this.schema != null ? this.schema : SchemaUtils.getSchema(message);
|
||||
return this.reactiveMessageSenderFactory.createSender(topic, schema,
|
||||
customizer == null ? Collections.emptyList() : Collections.singletonList(customizer));
|
||||
}
|
||||
|
||||
public static class SendMessageBuilderImpl<T> implements SendMessageBuilder<T> {
|
||||
|
||||
private final ReactivePulsarTemplate<T> template;
|
||||
|
||||
private final T message;
|
||||
|
||||
private String topic;
|
||||
|
||||
private MessageSpecBuilderCustomizer<T> messageCustomizer;
|
||||
|
||||
private ReactiveMessageSenderBuilderCustomizer<T> senderCustomizer;
|
||||
|
||||
SendMessageBuilderImpl(ReactivePulsarTemplate<T> template, T message) {
|
||||
this.template = template;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageBuilderImpl<T> withTopic(String topic) {
|
||||
this.topic = topic;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageBuilderImpl<T> withMessageCustomizer(MessageSpecBuilderCustomizer<T> messageCustomizer) {
|
||||
this.messageCustomizer = messageCustomizer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageBuilderImpl<T> withSenderCustomizer(
|
||||
ReactiveMessageSenderBuilderCustomizer<T> senderCustomizer) {
|
||||
this.senderCustomizer = senderCustomizer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<MessageId> send() {
|
||||
return this.template.doSend(this.topic, this.message, this.messageCustomizer, this.senderCustomizer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Package containing the core reactive components of the framework.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.pulsar.reactive.core;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -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.reactive.listener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumer;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessagePipeline;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessagePipelineBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessagePipelineBuilder.ConcurrentOneByOneMessagePipelineBuilder;
|
||||
import org.apache.pulsar.reactive.client.internal.api.ApiImplementationFactory;
|
||||
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Default implementation for {@link ReactivePulsarMessageListenerContainer}.
|
||||
*
|
||||
* @param <T> message type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public non-sealed class DefaultReactivePulsarMessageListenerContainer<T>
|
||||
implements ReactivePulsarMessageListenerContainer<T> {
|
||||
|
||||
private final LogAccessor logger = new LogAccessor(this.getClass());
|
||||
|
||||
private final ReactivePulsarConsumerFactory<T> pulsarConsumerFactory;
|
||||
|
||||
private final ReactivePulsarContainerProperties<T> pulsarContainerProperties;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
private ReactiveMessageConsumerBuilderCustomizer<T> consumerCustomizer;
|
||||
|
||||
private ReactiveMessagePipeline pipeline;
|
||||
|
||||
public DefaultReactivePulsarMessageListenerContainer(ReactivePulsarConsumerFactory<T> pulsarConsumerFactory,
|
||||
ReactivePulsarContainerProperties<T> pulsarContainerProperties) {
|
||||
this.pulsarConsumerFactory = pulsarConsumerFactory;
|
||||
this.pulsarContainerProperties = pulsarContainerProperties;
|
||||
}
|
||||
|
||||
public ReactivePulsarConsumerFactory<T> getReactivePulsarConsumerFactory() {
|
||||
return this.pulsarConsumerFactory;
|
||||
}
|
||||
|
||||
public ReactivePulsarContainerProperties<T> getContainerProperties() {
|
||||
return this.pulsarContainerProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running.get();
|
||||
}
|
||||
|
||||
protected void setRunning(boolean running) {
|
||||
this.running.set(running);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupMessageHandler(ReactivePulsarMessageHandler messageHandler) {
|
||||
this.pulsarContainerProperties.setMessageHandler(messageHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return this.autoStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutoStartup(boolean autoStartup) {
|
||||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
public ReactiveMessageConsumerBuilderCustomizer<T> getConsumerCustomizer() {
|
||||
return this.consumerCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConsumerCustomizer(ReactiveMessageConsumerBuilderCustomizer<T> consumerCustomizer) {
|
||||
this.consumerCustomizer = consumerCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void start() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (!isRunning()) {
|
||||
Objects.requireNonNull(this.pulsarContainerProperties.getMessageHandler(),
|
||||
"A ReactivePulsarMessageHandler must be provided");
|
||||
doStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (isRunning()) {
|
||||
doStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doStart() {
|
||||
setRunning(true);
|
||||
this.pipeline = startPipeline(this.pulsarContainerProperties);
|
||||
}
|
||||
|
||||
public void doStop() {
|
||||
try {
|
||||
this.logger.info("Closing Pulsar Reactive pipeline.");
|
||||
this.pipeline.close();
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.logger.error(e, () -> "Error closing Pulsar Reactive pipeline.");
|
||||
}
|
||||
finally {
|
||||
setRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
private ReactiveMessagePipeline startPipeline(ReactivePulsarContainerProperties<T> containerProperties) {
|
||||
ReactiveMessageConsumerBuilderCustomizer<T> customizer = (builder) -> {
|
||||
if (containerProperties.getSubscriptionType() != null) {
|
||||
builder.subscriptionType(containerProperties.getSubscriptionType());
|
||||
}
|
||||
if (containerProperties.getSubscriptionName() != null) {
|
||||
builder.subscriptionName(containerProperties.getSubscriptionName());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(containerProperties.getTopics())) {
|
||||
builder.topicNames(new ArrayList<>(containerProperties.getTopics()));
|
||||
}
|
||||
if (containerProperties.getTopicsPattern() != null) {
|
||||
builder.topicsPattern(containerProperties.getTopicsPattern());
|
||||
}
|
||||
};
|
||||
|
||||
List<ReactiveMessageConsumerBuilderCustomizer<T>> customizers = new ArrayList<>();
|
||||
customizers.add(customizer);
|
||||
if (this.consumerCustomizer != null) {
|
||||
customizers.add(this.consumerCustomizer);
|
||||
}
|
||||
|
||||
ReactiveMessageConsumer<T> consumer = getReactivePulsarConsumerFactory()
|
||||
.createConsumer(containerProperties.getSchema(), customizers);
|
||||
ReactiveMessagePipelineBuilder<T> pipelineBuilder = ApiImplementationFactory
|
||||
.createReactiveMessageHandlerPipelineBuilder(consumer);
|
||||
Object messageHandler = containerProperties.getMessageHandler();
|
||||
ReactiveMessagePipeline pipeline;
|
||||
if (messageHandler instanceof ReactivePulsarStreamingHandler<?>) {
|
||||
pipeline = pipelineBuilder
|
||||
.streamingMessageHandler(((ReactivePulsarStreamingHandler<T>) messageHandler)::received).build();
|
||||
}
|
||||
else {
|
||||
ReactiveMessagePipelineBuilder.OneByOneMessagePipelineBuilder<T> messagePipelineBuilder = pipelineBuilder
|
||||
.messageHandler(((ReactivePulsarOneByOneMessageHandler<T>) messageHandler)::received)
|
||||
.handlingTimeout(containerProperties.getHandlingTimeout());
|
||||
if (containerProperties.getConcurrency() > 0) {
|
||||
ConcurrentOneByOneMessagePipelineBuilder<T> concurrentPipelineBuilder = messagePipelineBuilder
|
||||
.concurrent().concurrency(containerProperties.getConcurrency());
|
||||
if (containerProperties.isUseKeyOrderedProcessing()) {
|
||||
concurrentPipelineBuilder.useKeyOrderedProcessing();
|
||||
}
|
||||
pipeline = concurrentPipelineBuilder.build();
|
||||
}
|
||||
else {
|
||||
pipeline = pipelineBuilder.build();
|
||||
}
|
||||
}
|
||||
pipeline.start();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.reactive.listener;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
/**
|
||||
* Contains runtime properties for a reactive listener container.
|
||||
*
|
||||
* @param <T> message type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class ReactivePulsarContainerProperties<T> {
|
||||
|
||||
private Collection<String> topics;
|
||||
|
||||
private Pattern topicsPattern;
|
||||
|
||||
private String subscriptionName;
|
||||
|
||||
private SubscriptionType subscriptionType = SubscriptionType.Exclusive;
|
||||
|
||||
private Schema<T> schema;
|
||||
|
||||
private SchemaType schemaType;
|
||||
|
||||
private ReactivePulsarMessageHandler messageHandler;
|
||||
|
||||
private Duration handlingTimeout = Duration.ofMinutes(2);
|
||||
|
||||
private int concurrency = 0;
|
||||
|
||||
private boolean useKeyOrderedProcessing = false;
|
||||
|
||||
public ReactivePulsarMessageHandler getMessageHandler() {
|
||||
return this.messageHandler;
|
||||
}
|
||||
|
||||
public void setMessageHandler(ReactivePulsarMessageHandler messageHandler) {
|
||||
this.messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
public SubscriptionType getSubscriptionType() {
|
||||
return this.subscriptionType;
|
||||
}
|
||||
|
||||
public void setSubscriptionType(SubscriptionType subscriptionType) {
|
||||
this.subscriptionType = subscriptionType;
|
||||
}
|
||||
|
||||
public Schema<T> getSchema() {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
public void setSchema(Schema<T> schema) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
public SchemaType getSchemaType() {
|
||||
return this.schemaType;
|
||||
}
|
||||
|
||||
public void setSchemaType(SchemaType schemaType) {
|
||||
this.schemaType = schemaType;
|
||||
}
|
||||
|
||||
public Collection<String> getTopics() {
|
||||
return this.topics;
|
||||
}
|
||||
|
||||
public void setTopics(Collection<String> topics) {
|
||||
this.topics = topics;
|
||||
}
|
||||
|
||||
public Pattern getTopicsPattern() {
|
||||
return this.topicsPattern;
|
||||
}
|
||||
|
||||
public void setTopicsPattern(Pattern topicsPattern) {
|
||||
this.topicsPattern = topicsPattern;
|
||||
}
|
||||
|
||||
public void setTopicsPattern(String topicsPattern) {
|
||||
this.topicsPattern = Pattern.compile(topicsPattern);
|
||||
}
|
||||
|
||||
public String getSubscriptionName() {
|
||||
return this.subscriptionName;
|
||||
}
|
||||
|
||||
public void setSubscriptionName(String subscriptionName) {
|
||||
this.subscriptionName = subscriptionName;
|
||||
}
|
||||
|
||||
public Duration getHandlingTimeout() {
|
||||
return this.handlingTimeout;
|
||||
}
|
||||
|
||||
public void setHandlingTimeout(Duration handlingTimeout) {
|
||||
this.handlingTimeout = handlingTimeout;
|
||||
}
|
||||
|
||||
public int getConcurrency() {
|
||||
return this.concurrency;
|
||||
}
|
||||
|
||||
public void setConcurrency(int concurrency) {
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
public boolean isUseKeyOrderedProcessing() {
|
||||
return this.useKeyOrderedProcessing;
|
||||
}
|
||||
|
||||
public void setUseKeyOrderedProcessing(boolean useKeyOrderedProcessing) {
|
||||
this.useKeyOrderedProcessing = useKeyOrderedProcessing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.reactive.listener;
|
||||
|
||||
/**
|
||||
* Reactive message handler used by {@link DefaultReactivePulsarMessageListenerContainer}.
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public sealed interface ReactivePulsarMessageHandler permits ReactivePulsarOneByOneMessageHandler, ReactivePulsarStreamingHandler {
|
||||
|
||||
}
|
||||
@@ -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.reactive.listener;
|
||||
|
||||
import org.springframework.pulsar.listener.MessageListenerContainer;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
|
||||
/**
|
||||
* Internal abstraction used by the framework representing a reactive message listener
|
||||
* container. Not meant to be implemented externally.
|
||||
*
|
||||
* @param <T> message type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public sealed interface ReactivePulsarMessageListenerContainer<T>
|
||||
extends MessageListenerContainer permits DefaultReactivePulsarMessageListenerContainer {
|
||||
|
||||
void setupMessageHandler(ReactivePulsarMessageHandler messageListener);
|
||||
|
||||
default ReactivePulsarContainerProperties<T> getContainerProperties() {
|
||||
throw new UnsupportedOperationException("This container doesn't support retrieving its properties");
|
||||
}
|
||||
|
||||
void setConsumerCustomizer(ReactiveMessageConsumerBuilderCustomizer<T> consumerCustomizer);
|
||||
|
||||
}
|
||||
@@ -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.reactive.listener;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessagePipelineBuilder;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
/**
|
||||
* Message handler class with a {@link #received} method for use in
|
||||
* {@link ReactiveMessagePipelineBuilder#messageHandler}.
|
||||
*
|
||||
* @param <T> message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public non-sealed interface ReactivePulsarOneByOneMessageHandler<T> extends ReactivePulsarMessageHandler {
|
||||
|
||||
/**
|
||||
* Callback passed to {@link ReactiveMessagePipelineBuilder#messageHandler} that will
|
||||
* be called for each received message.
|
||||
* @param message the message received
|
||||
* @return a completed {@link Publisher} when the callback is done.
|
||||
*/
|
||||
Publisher<Void> received(Message<T> message);
|
||||
|
||||
}
|
||||
@@ -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.reactive.listener;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.reactive.client.api.MessageResult;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessagePipelineBuilder;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Message handler class with a {@link #received} method for use in
|
||||
* {@link ReactiveMessagePipelineBuilder#streamingMessageHandler}.
|
||||
*
|
||||
* @param <T> message payload type
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public non-sealed interface ReactivePulsarStreamingHandler<T> extends ReactivePulsarMessageHandler {
|
||||
|
||||
/**
|
||||
* Callback passed to {@link ReactiveMessagePipelineBuilder#streamingMessageHandler}
|
||||
* that will be applied to the flux of received message.
|
||||
* @param messages the messages received
|
||||
* @return a completed {@link Publisher} when the callback is done.
|
||||
*/
|
||||
Publisher<MessageResult<Void>> received(Flux<Message<T>> messages);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.reactive.listener.adapter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.pulsar.listener.adapter.HandlerAdapter;
|
||||
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageHandler;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarOneByOneMessageHandler;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* A {@link ReactivePulsarMessageHandler MessageListener} adapter that invokes a
|
||||
* configurable {@link HandlerAdapter}; used when the factory is configured for the
|
||||
* listener to receive individual messages.
|
||||
*
|
||||
* @param <V> payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class PulsarReactiveOneByOneMessagingMessageListenerAdapter<V> extends PulsarMessagingMessageListenerAdapter<V>
|
||||
implements ReactivePulsarOneByOneMessageHandler<V> {
|
||||
|
||||
public PulsarReactiveOneByOneMessagingMessageListenerAdapter(Object bean, Method method) {
|
||||
super(bean, method);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Publisher<Void> received(Message<V> record) {
|
||||
org.springframework.messaging.Message<?> message = null;
|
||||
Object theRecord = record;
|
||||
if (isHeaderFound() || isSpringMessage()) {
|
||||
message = toMessagingMessage(record, null);
|
||||
}
|
||||
else if (isSimpleExtraction()) {
|
||||
theRecord = record.getValue();
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
this.logger.debug("Processing [" + message + "]");
|
||||
}
|
||||
try {
|
||||
return (Mono<Void>) invokeHandler(theRecord, message, null, null);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.reactive.listener.adapter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.reactive.client.api.MessageResult;
|
||||
|
||||
import org.springframework.pulsar.listener.adapter.HandlerAdapter;
|
||||
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarMessageHandler;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarStreamingHandler;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* A {@link ReactivePulsarMessageHandler MessageListener} adapter that invokes a
|
||||
* configurable {@link HandlerAdapter}; used when the factory is configured for the
|
||||
* listener to receive a flux of messages.
|
||||
*
|
||||
* @param <V> payload type.
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
public class PulsarReactiveStreamingMessagingMessageListenerAdapter<V> extends PulsarMessagingMessageListenerAdapter<V>
|
||||
implements ReactivePulsarStreamingHandler<V> {
|
||||
|
||||
public PulsarReactiveStreamingMessagingMessageListenerAdapter(Object bean, Method method) {
|
||||
super(bean, method);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<MessageResult<Void>> received(Flux<Message<V>> records) {
|
||||
Flux<?> theRecords = records;
|
||||
if (isSpringMessageFlux()) {
|
||||
theRecords = records.map(record -> toMessagingMessage(record, null));
|
||||
}
|
||||
try {
|
||||
return (Flux<MessageResult<Void>>) invokeHandler(theRecords, null, null, null);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.reactive.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;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.pulsar.listener.Acknowledgement;
|
||||
import org.springframework.pulsar.listener.PulsarAcknowledgingMessageListener;
|
||||
import org.springframework.pulsar.listener.adapter.HandlerAdapter;
|
||||
import org.springframework.pulsar.listener.adapter.PulsarMessagingMessageListenerAdapter;
|
||||
|
||||
/**
|
||||
* A {@link MessageListener MessageListener} adapter that invokes a configurable
|
||||
* {@link HandlerAdapter}; used when the factory is configured for the listener to receive
|
||||
* individual messages.
|
||||
*
|
||||
* @param <V> payload type.
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class PulsarRecordMessagingMessageListenerAdapter<V> extends PulsarMessagingMessageListenerAdapter<V>
|
||||
implements PulsarAcknowledgingMessageListener<V> {
|
||||
|
||||
public PulsarRecordMessagingMessageListenerAdapter(Object bean, Method method) {
|
||||
super(bean, method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void received(Consumer<V> consumer, Message<V> record, @Nullable Acknowledgement acknowledgement) {
|
||||
org.springframework.messaging.Message<?> message = null;
|
||||
Object theRecord = record;
|
||||
if (isHeaderFound() || isSpringMessage()) {
|
||||
message = toMessagingMessage(record, consumer);
|
||||
}
|
||||
else if (isSimpleExtraction()) {
|
||||
theRecord = record.getValue();
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
this.logger.debug("Processing [" + message + "]");
|
||||
}
|
||||
try {
|
||||
invokeHandler(theRecord, message, consumer, acknowledgement);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Package containing listener components for receiving Pulsar messages.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.pulsar.reactive.listener.adapter;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Package containing listener components for receiving Pulsar messages.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.pulsar.reactive.listener;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.pulsar.reactive.aot.ReactivePulsarRuntimeHints
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.Locale;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.testcontainers.containers.PulsarContainer;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
/**
|
||||
* Provides a static {@link PulsarContainer} that can be shared across test classes.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
public interface PulsarTestContainerSupport {
|
||||
|
||||
PulsarContainer PULSAR_CONTAINER = new PulsarContainer(
|
||||
isRunningOnMacM1() ? getMacM1PulsarImage() : getStandardPulsarImage());
|
||||
|
||||
@BeforeAll
|
||||
static void startContainer() {
|
||||
PULSAR_CONTAINER.start();
|
||||
}
|
||||
|
||||
static String getPulsarBrokerUrl() {
|
||||
return PULSAR_CONTAINER.getPulsarBrokerUrl();
|
||||
}
|
||||
|
||||
static String getHttpServiceUrl() {
|
||||
return PULSAR_CONTAINER.getHttpServiceUrl();
|
||||
}
|
||||
|
||||
private static boolean isRunningOnMacM1() {
|
||||
String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
|
||||
String osArchitecture = System.getProperty("os.arch").toLowerCase(Locale.ENGLISH);
|
||||
return osName.contains("mac") && osArchitecture.equals("aarch64");
|
||||
}
|
||||
|
||||
private static DockerImageName getStandardPulsarImage() {
|
||||
return DockerImageName.parse("apachepulsar/pulsar:2.10.1");
|
||||
}
|
||||
|
||||
private static DockerImageName getMacM1PulsarImage() {
|
||||
return DockerImageName.parse("kezhenxu94/pulsar").asCompatibleSubstituteFor("apachepulsar/pulsar");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageConsumerSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumer;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerSpec;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for
|
||||
* {@link org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory}
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class DefaultReactiveMessageConsumerFactoryTests {
|
||||
|
||||
private static final Schema<String> SCHEMA = Schema.STRING;
|
||||
|
||||
@Nested
|
||||
class FactoryCreatedWithoutSpec {
|
||||
|
||||
private org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory<String> consumerFactory = new org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory<>(
|
||||
AdaptedReactivePulsarClientFactory.create((PulsarClient) null), null);
|
||||
|
||||
@Test
|
||||
void createConsumer() {
|
||||
ReactiveMessageConsumer<String> consumer = consumerFactory.createConsumer(SCHEMA);
|
||||
|
||||
assertThat(consumer)
|
||||
.extracting("consumerSpec", InstanceOfAssertFactories.type(ReactiveMessageConsumerSpec.class))
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConsumerWithCustomizer() {
|
||||
ReactiveMessageConsumer<String> consumer = consumerFactory.createConsumer(SCHEMA,
|
||||
Collections.singletonList(builder -> builder.consumerName("new-test-consumer")));
|
||||
|
||||
assertThat(consumer)
|
||||
.extracting("consumerSpec", InstanceOfAssertFactories.type(ReactiveMessageConsumerSpec.class))
|
||||
.extracting(ReactiveMessageConsumerSpec::getConsumerName).isEqualTo("new-test-consumer");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FactoryCreatedWithSpec {
|
||||
|
||||
private org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory<String> consumerFactory;
|
||||
|
||||
@BeforeEach
|
||||
void createConsumerFactory() {
|
||||
MutableReactiveMessageConsumerSpec spec = new MutableReactiveMessageConsumerSpec();
|
||||
spec.setConsumerName("test-consumer");
|
||||
consumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
AdaptedReactivePulsarClientFactory.create((PulsarClient) null), spec);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConsumer() {
|
||||
ReactiveMessageConsumer<String> consumer = consumerFactory.createConsumer(SCHEMA);
|
||||
|
||||
assertThat(consumer)
|
||||
.extracting("consumerSpec", InstanceOfAssertFactories.type(ReactiveMessageConsumerSpec.class))
|
||||
.extracting(ReactiveMessageConsumerSpec::getConsumerName).isEqualTo("test-consumer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createConsumerWithCustomizer() {
|
||||
ReactiveMessageConsumer<String> consumer = consumerFactory.createConsumer(SCHEMA,
|
||||
Collections.singletonList(builder -> builder.consumerName("new-test-consumer")));
|
||||
|
||||
assertThat(consumer)
|
||||
.extracting("consumerSpec", InstanceOfAssertFactories.type(ReactiveMessageConsumerSpec.class))
|
||||
.extracting(ReactiveMessageConsumerSpec::getConsumerName).isEqualTo("new-test-consumer");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageReaderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReader;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderSpec;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for
|
||||
* {@link org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory}
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
class DefaultReactiveMessageReaderFactoryTests {
|
||||
|
||||
private static final Schema<String> schema = Schema.STRING;
|
||||
|
||||
@Test
|
||||
void createReader() {
|
||||
MutableReactiveMessageReaderSpec spec = new MutableReactiveMessageReaderSpec();
|
||||
spec.setReaderName("test-reader");
|
||||
org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory<String> readerFactory = new org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory<>(
|
||||
AdaptedReactivePulsarClientFactory.create((PulsarClient) null), spec);
|
||||
|
||||
ReactiveMessageReader<String> reader = readerFactory.createReader(schema);
|
||||
|
||||
assertThat(reader).extracting("readerSpec", InstanceOfAssertFactories.type(ReactiveMessageReaderSpec.class))
|
||||
.extracting(ReactiveMessageReaderSpec::getReaderName).isEqualTo("test-reader");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createReaderWithCustomizer() {
|
||||
MutableReactiveMessageReaderSpec spec = new MutableReactiveMessageReaderSpec();
|
||||
spec.setReaderName("test-reader");
|
||||
org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory<String> readerFactory = new DefaultReactivePulsarReaderFactory<>(
|
||||
AdaptedReactivePulsarClientFactory.create((PulsarClient) null), spec);
|
||||
|
||||
ReactiveMessageReader<String> reader = readerFactory.createReader(schema,
|
||||
Collections.singletonList(builder -> builder.readerName("new-test-reader")));
|
||||
|
||||
assertThat(reader).extracting("readerSpec", InstanceOfAssertFactories.type(ReactiveMessageReaderSpec.class))
|
||||
.extracting(ReactiveMessageReaderSpec::getReaderName).isEqualTo("new-test-reader");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageSenderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSender;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderSpec;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for
|
||||
* {@link org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory}
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
class DefaultReactiveMessageSenderFactoryTests {
|
||||
|
||||
protected final Schema<String> schema = Schema.STRING;
|
||||
|
||||
@Test
|
||||
void createSenderWithSpecificTopic() {
|
||||
testCreateSender(null, null, "topic1", null, "topic1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSenderWithDefaultTopic() {
|
||||
MutableReactiveMessageSenderSpec senderSpec = new MutableReactiveMessageSenderSpec();
|
||||
senderSpec.setTopicName("topic0");
|
||||
|
||||
testCreateSender(senderSpec, null, null, null, "topic0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSenderWithSingleSenderCustomizer() {
|
||||
testCreateSender(null, null, "topic1", Collections.singletonList(builder -> builder.topic("topic1")), "topic1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSenderWithMultipleSenderCustomizer() {
|
||||
org.springframework.pulsar.reactive.core.ReactiveMessageSenderBuilderCustomizer<String> customizer1 = builder -> builder
|
||||
.topic("topic1");
|
||||
ReactiveMessageSenderCache cache = AdaptedReactivePulsarClientFactory.createCache();
|
||||
org.springframework.pulsar.reactive.core.ReactiveMessageSenderBuilderCustomizer<String> customizer2 = builder -> builder
|
||||
.cache(cache);
|
||||
|
||||
ReactiveMessageSender<String> sender = testCreateSender(null, null, "topic0",
|
||||
Arrays.asList(customizer1, customizer2), "topic1");
|
||||
assertThat(sender).extracting("producerCache").isSameAs(cache);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSenderWithNoTopic() {
|
||||
org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory<String> senderFactory = new org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory<>(
|
||||
(PulsarClient) null, null, null);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> senderFactory.createSender(null, schema))
|
||||
.withMessageContaining("Topic must be specified when no default topic is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSenderWithCache() {
|
||||
ReactiveMessageSenderCache cache = AdaptedReactivePulsarClientFactory.createCache();
|
||||
ReactiveMessageSender<String> sender = testCreateSender(null, cache, "topic1", null, "topic1");
|
||||
assertThat(sender).extracting("producerCache").isSameAs(cache);
|
||||
}
|
||||
|
||||
private ReactiveMessageSender<String> testCreateSender(ReactiveMessageSenderSpec spec,
|
||||
ReactiveMessageSenderCache cache, String topic,
|
||||
List<ReactiveMessageSenderBuilderCustomizer<String>> customizers, String expectedTopic) {
|
||||
ReactivePulsarSenderFactory<String> senderFactory = new DefaultReactivePulsarSenderFactory<>(
|
||||
(PulsarClient) null, spec, cache);
|
||||
ReactiveMessageSender<String> sender = senderFactory.createSender(topic, schema, customizers);
|
||||
assertThat(sender).extracting("senderSpec", InstanceOfAssertFactories.type(ReactiveMessageSenderSpec.class))
|
||||
.extracting(ReactiveMessageSenderSpec::getTopicName).isEqualTo(expectedTopic);
|
||||
return sender;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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.reactive.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageSenderSpec;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.pulsar.core.PulsarTestContainerSupport;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Tests for {@link org.springframework.pulsar.reactive.core.ReactivePulsarTemplate}.
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
class ReactivePulsarTemplateTests implements PulsarTestContainerSupport {
|
||||
|
||||
@Test
|
||||
void sendMessagesWithSpecificSchemaTest() throws Exception {
|
||||
String topic = "smt-specific-schema-topic-reactive";
|
||||
try (PulsarClient client = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build()) {
|
||||
try (Consumer<Foo> consumer = client.newConsumer(Schema.JSON(Foo.class)).topic(topic)
|
||||
.subscriptionName("test-specific-schema-subscription").subscribe()) {
|
||||
MutableReactiveMessageSenderSpec senderSpec = new MutableReactiveMessageSenderSpec();
|
||||
senderSpec.setTopicName(topic);
|
||||
org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory<Foo> producerFactory = new org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory<>(
|
||||
client, senderSpec, null);
|
||||
org.springframework.pulsar.reactive.core.ReactivePulsarTemplate<Foo> pulsarTemplate = new org.springframework.pulsar.reactive.core.ReactivePulsarTemplate<>(
|
||||
producerFactory);
|
||||
pulsarTemplate.setSchema(Schema.JSON(Foo.class));
|
||||
|
||||
List<Foo> foos = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
foos.add(new Foo("Foo-" + UUID.randomUUID(), "Bar-" + UUID.randomUUID()));
|
||||
}
|
||||
pulsarTemplate.send(Flux.fromIterable(foos)).subscribe();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
assertThat(consumer.receiveAsync()).succeedsWithin(Duration.ofSeconds(3))
|
||||
.extracting(Message::getValue).isEqualTo(foos.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("sendMessageTestProvider")
|
||||
void sendMessageTest(String testName, SendTestArgs testArgs) throws Exception {
|
||||
// Use the test args to construct the params to pass to send handler
|
||||
String topic = testName;
|
||||
String subscription = topic + "-sub";
|
||||
String msgPayload = topic + "-msg";
|
||||
MessageSpecBuilderCustomizer<String> messageCustomizer = null;
|
||||
if (testArgs.useMessageCustomizer) {
|
||||
messageCustomizer = (mb) -> mb.key("foo-key");
|
||||
}
|
||||
ReactiveMessageSenderBuilderCustomizer<String> senderCustomizer = null;
|
||||
if (testArgs.useSenderCustomizer) {
|
||||
senderCustomizer = (sb) -> sb.producerName("foo-producer");
|
||||
}
|
||||
try (PulsarClient client = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build()) {
|
||||
try (Consumer<String> consumer = client.newConsumer(Schema.STRING).topic(topic)
|
||||
.subscriptionName(subscription).subscribe()) {
|
||||
MutableReactiveMessageSenderSpec senderSpec = new MutableReactiveMessageSenderSpec();
|
||||
if (!testArgs.useSpecificTopic) {
|
||||
senderSpec.setTopicName(topic);
|
||||
}
|
||||
ReactivePulsarSenderFactory<String> senderFactory = new DefaultReactivePulsarSenderFactory<>(client,
|
||||
senderSpec, null);
|
||||
org.springframework.pulsar.reactive.core.ReactivePulsarTemplate<String> pulsarTemplate = new org.springframework.pulsar.reactive.core.ReactivePulsarTemplate<>(
|
||||
senderFactory);
|
||||
Mono<MessageId> sendResponse;
|
||||
if (testArgs.useTemplateSchema) {
|
||||
pulsarTemplate.setSchema(Schema.STRING);
|
||||
}
|
||||
if (testArgs.useSimpleApi) {
|
||||
sendResponse = testArgs.useSpecificTopic ? pulsarTemplate.send(topic, msgPayload)
|
||||
: pulsarTemplate.send(msgPayload);
|
||||
}
|
||||
else {
|
||||
ReactivePulsarTemplate.SendMessageBuilderImpl<String> messageBuilder = pulsarTemplate
|
||||
.newMessage(msgPayload);
|
||||
if (testArgs.useSpecificTopic) {
|
||||
messageBuilder = messageBuilder.withTopic(topic);
|
||||
}
|
||||
if (messageCustomizer != null) {
|
||||
messageBuilder = messageBuilder.withMessageCustomizer(messageCustomizer);
|
||||
}
|
||||
if (senderCustomizer != null) {
|
||||
messageBuilder = messageBuilder.withSenderCustomizer(senderCustomizer);
|
||||
}
|
||||
sendResponse = messageBuilder.send();
|
||||
}
|
||||
sendResponse.subscribe();
|
||||
|
||||
Message<String> msg = consumer.receive(3, TimeUnit.SECONDS);
|
||||
|
||||
assertThat(msg).isNotNull();
|
||||
assertThat(msg.getData()).asString().isEqualTo(msgPayload);
|
||||
if (messageCustomizer != null) {
|
||||
assertThat(msg.getKey()).isEqualTo("foo-key");
|
||||
}
|
||||
if (senderCustomizer != null) {
|
||||
assertThat(msg.getProducerName()).isEqualTo("foo-producer");
|
||||
}
|
||||
// Make sure the producer was closed by the template (albeit indirectly as
|
||||
// client removes closed producers)
|
||||
await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertThat(client).extracting("producers")
|
||||
.asInstanceOf(InstanceOfAssertFactories.COLLECTION).isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> sendMessageTestProvider() {
|
||||
return Stream.of(arguments("sendReactiveMessageToDefaultTopic", SendTestArgs.useSpecificTopic(false)),
|
||||
arguments("sendReactiveMessageToDefaultTopicWithSimpleApi",
|
||||
SendTestArgs.useSpecificTopic(false).useSimpleApi()),
|
||||
arguments("sendReactiveMessageToDefaultTopicWithSimpleApiAndTemplateSchema",
|
||||
SendTestArgs.useSpecificTopic(false).useSimpleApi().useTemplateSchema()),
|
||||
arguments("sendReactiveMessageToDefaultTopicWithMessageCustomizer",
|
||||
SendTestArgs.useSpecificTopic(false).useMessageCustomizer()),
|
||||
arguments("sendReactiveMessageToDefaultTopicWithProducerCustomizer",
|
||||
SendTestArgs.useSpecificTopic(false).useSenderCustomizer()),
|
||||
arguments("sendReactiveMessageToDefaultTopicWithAllOptions",
|
||||
SendTestArgs.useSpecificTopic(false).useMessageCustomizer().useSenderCustomizer()),
|
||||
arguments("sendReactiveMessageToSpecificTopic", SendTestArgs.useSpecificTopic(true)),
|
||||
arguments("sendReactiveMessageToSpecificTopicWithSimpleApi",
|
||||
SendTestArgs.useSpecificTopic(true).useSimpleApi()),
|
||||
arguments("sendReactiveMessageToSpecificTopicWithSimpleApiAndTemplateSchema",
|
||||
SendTestArgs.useSpecificTopic(true).useSimpleApi().useTemplateSchema()),
|
||||
arguments("sendReactiveMessageToSpecificTopicWithMessageCustomizer",
|
||||
SendTestArgs.useSpecificTopic(true).useMessageCustomizer()),
|
||||
arguments("sendReactiveMessageToSpecificTopicWithProducerCustomizer",
|
||||
SendTestArgs.useSpecificTopic(true).useSenderCustomizer()),
|
||||
arguments("sendReactiveMessageToSpecificTopicWithAllOptions",
|
||||
SendTestArgs.useSpecificTopic(true).useMessageCustomizer().useSenderCustomizer()));
|
||||
}
|
||||
|
||||
static final class SendTestArgs {
|
||||
|
||||
private final boolean useSpecificTopic;
|
||||
|
||||
private boolean useMessageCustomizer;
|
||||
|
||||
private boolean useSenderCustomizer;
|
||||
|
||||
private boolean useSimpleApi;
|
||||
|
||||
private boolean useTemplateSchema;
|
||||
|
||||
private SendTestArgs(boolean useSpecificTopic) {
|
||||
this.useSpecificTopic = useSpecificTopic;
|
||||
}
|
||||
|
||||
static SendTestArgs useSpecificTopic(boolean useSpecificTopic) {
|
||||
return new SendTestArgs(useSpecificTopic);
|
||||
}
|
||||
|
||||
SendTestArgs useMessageCustomizer() {
|
||||
this.useMessageCustomizer = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
SendTestArgs useSenderCustomizer() {
|
||||
this.useSenderCustomizer = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
SendTestArgs useSimpleApi() {
|
||||
this.useSimpleApi = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
SendTestArgs useTemplateSchema() {
|
||||
this.useTemplateSchema = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Foo(String foo, String bar) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* 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.reactive.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
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.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.adapter.DefaultMessageGroupingFunction;
|
||||
import org.apache.pulsar.reactive.client.api.MessageResult;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageConsumerSpec;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageSenderSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumer;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessagePipeline;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.pulsar.core.PulsarTestContainerSupport;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultReactivePulsarMessageListenerContainer}
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
class DefaultReactivePulsarMessageListenerContainerTests implements PulsarTestContainerSupport {
|
||||
|
||||
@Test
|
||||
void messageHandlerListener() throws Exception {
|
||||
String topic = "drpmlct-012";
|
||||
MutableReactiveMessageConsumerSpec config = new MutableReactiveMessageConsumerSpec();
|
||||
config.setTopicNames(Collections.singletonList(topic));
|
||||
config.setSubscriptionName("drpmlct-sb-012");
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, config);
|
||||
// Ensure subscription is created
|
||||
pulsarConsumerFactory.createConsumer(Schema.STRING).consumeNothing().block(Duration.ofSeconds(10));
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties.setMessageHandler(
|
||||
(ReactivePulsarOneByOneMessageHandler<String>) (msg) -> Mono.fromRunnable(latch::countDown));
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
container.start();
|
||||
MutableReactiveMessageSenderSpec prodConfig = new MutableReactiveMessageSenderSpec();
|
||||
prodConfig.setTopicName(topic);
|
||||
DefaultReactivePulsarSenderFactory<String> pulsarProducerFactory = new DefaultReactivePulsarSenderFactory<>(
|
||||
reactivePulsarClient, prodConfig, null);
|
||||
ReactivePulsarTemplate<String> pulsarTemplate = new ReactivePulsarTemplate<>(pulsarProducerFactory);
|
||||
pulsarTemplate.send("hello john doe").subscribe();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamingHandlerListener() throws Exception {
|
||||
String topic = "drpmlct-013";
|
||||
MutableReactiveMessageConsumerSpec config = new MutableReactiveMessageConsumerSpec();
|
||||
config.setTopicNames(Collections.singletonList(topic));
|
||||
config.setSubscriptionName("drpmlct-sb-013");
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, config);
|
||||
// Ensure subscription is created
|
||||
pulsarConsumerFactory.createConsumer(Schema.STRING).consumeNothing().block(Duration.ofSeconds(10));
|
||||
CountDownLatch latch = new CountDownLatch(5);
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties.setMessageHandler((ReactivePulsarStreamingHandler<String>) (msg) -> msg.map(m -> {
|
||||
latch.countDown();
|
||||
return MessageResult.acknowledge(m.getMessageId());
|
||||
}));
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
container.start();
|
||||
MutableReactiveMessageSenderSpec prodConfig = new MutableReactiveMessageSenderSpec();
|
||||
prodConfig.setTopicName(topic);
|
||||
DefaultReactivePulsarSenderFactory<String> pulsarProducerFactory = new DefaultReactivePulsarSenderFactory<>(
|
||||
reactivePulsarClient, prodConfig, null);
|
||||
ReactivePulsarTemplate<String> pulsarTemplate = new ReactivePulsarTemplate<>(pulsarProducerFactory);
|
||||
Flux.range(0, 5).map(i -> "hello john doe" + i).as(pulsarTemplate::send).subscribe();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerProperties() throws Exception {
|
||||
String topic = "drpmlct-sb-014";
|
||||
String subscriptionName = "drpmlct-sb-014";
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, null);
|
||||
// Ensure subscription is created
|
||||
pulsarConsumerFactory
|
||||
.createConsumer(Schema.STRING,
|
||||
Collections.singletonList(
|
||||
c -> c.topicNames(Collections.singletonList(topic)).subscriptionName(subscriptionName)))
|
||||
.consumeNothing().block(Duration.ofSeconds(10));
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties.setMessageHandler(
|
||||
(ReactivePulsarOneByOneMessageHandler<String>) (msg) -> Mono.fromRunnable(latch::countDown));
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
pulsarContainerProperties.setTopics(List.of(topic));
|
||||
pulsarContainerProperties.setSubscriptionName(subscriptionName);
|
||||
pulsarContainerProperties.setConcurrency(5);
|
||||
pulsarContainerProperties.setUseKeyOrderedProcessing(true);
|
||||
pulsarContainerProperties.setHandlingTimeout(Duration.ofMillis(7));
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
container.start();
|
||||
MutableReactiveMessageSenderSpec prodConfig = new MutableReactiveMessageSenderSpec();
|
||||
prodConfig.setTopicName(topic);
|
||||
DefaultReactivePulsarSenderFactory<String> pulsarProducerFactory = new DefaultReactivePulsarSenderFactory<>(
|
||||
reactivePulsarClient, prodConfig, null);
|
||||
ReactivePulsarTemplate<String> pulsarTemplate = new ReactivePulsarTemplate<>(pulsarProducerFactory);
|
||||
pulsarTemplate.send("hello john doe").subscribe();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
assertThat(container).extracting("pipeline", InstanceOfAssertFactories.type(ReactiveMessagePipeline.class))
|
||||
.hasFieldOrPropertyWithValue("concurrency", 5)
|
||||
.hasFieldOrPropertyWithValue("handlingTimeout", Duration.ofMillis(7)).extracting("groupingFunction")
|
||||
.isInstanceOf(DefaultMessageGroupingFunction.class);
|
||||
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultSubscriptionType() throws Exception {
|
||||
String topic = "drpmlct-015";
|
||||
MutableReactiveMessageConsumerSpec config = new MutableReactiveMessageConsumerSpec();
|
||||
config.setTopicNames(Collections.singletonList(topic));
|
||||
config.setSubscriptionName("drpmlct-sb-015");
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, config);
|
||||
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties
|
||||
.setMessageHandler((ReactivePulsarOneByOneMessageHandler<String>) (msg) -> Mono.empty());
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
container.start();
|
||||
|
||||
Thread.sleep(2_000);
|
||||
|
||||
StepVerifier
|
||||
.create(pulsarConsumerFactory
|
||||
.createConsumer(Schema.STRING,
|
||||
Collections.singletonList(c -> c.subscriptionType(SubscriptionType.Shared)))
|
||||
.consumeNothing())
|
||||
.expectError().verify(Duration.ofSeconds(10));
|
||||
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerSubscriptionType() throws Exception {
|
||||
String topic = "drpmlct-016";
|
||||
MutableReactiveMessageConsumerSpec config = new MutableReactiveMessageConsumerSpec();
|
||||
config.setTopicNames(Collections.singletonList(topic));
|
||||
config.setSubscriptionName("drpmlct-sb-016");
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, config);
|
||||
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties
|
||||
.setMessageHandler((ReactivePulsarOneByOneMessageHandler<String>) (msg) -> Mono.empty());
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
pulsarContainerProperties.setSubscriptionType(SubscriptionType.Shared);
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
container.start();
|
||||
|
||||
Thread.sleep(2_000);
|
||||
|
||||
StepVerifier
|
||||
.create(pulsarConsumerFactory
|
||||
.createConsumer(Schema.STRING,
|
||||
Collections.singletonList(c -> c.subscriptionType(SubscriptionType.Shared)))
|
||||
.consumeNothing())
|
||||
.expectComplete().verify(Duration.ofSeconds(10));
|
||||
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerTopicsPattern() throws Exception {
|
||||
String topic = "drpmlct-017-foo";
|
||||
String subscriptionName = "drpmlct-sb-017";
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, null);
|
||||
// Ensure subscription is created
|
||||
pulsarConsumerFactory
|
||||
.createConsumer(Schema.STRING,
|
||||
Collections.singletonList(
|
||||
c -> c.topicNames(Collections.singletonList(topic)).subscriptionName(subscriptionName)))
|
||||
.consumeNothing().block(Duration.ofSeconds(10));
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties.setMessageHandler(
|
||||
(ReactivePulsarOneByOneMessageHandler<String>) (msg) -> Mono.fromRunnable(latch::countDown));
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
pulsarContainerProperties.setTopicsPattern("persistent://public/default/drpmlct-017-.*");
|
||||
pulsarContainerProperties.setSubscriptionName(subscriptionName);
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
container.start();
|
||||
MutableReactiveMessageSenderSpec prodConfig = new MutableReactiveMessageSenderSpec();
|
||||
prodConfig.setTopicName(topic);
|
||||
DefaultReactivePulsarSenderFactory<String> pulsarProducerFactory = new DefaultReactivePulsarSenderFactory<>(
|
||||
reactivePulsarClient, prodConfig, null);
|
||||
ReactivePulsarTemplate<String> pulsarTemplate = new ReactivePulsarTemplate<>(pulsarProducerFactory);
|
||||
pulsarTemplate.send("hello john doe").subscribe();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumerCustomizer() throws Exception {
|
||||
String topic = "drpmlct-018";
|
||||
String deadLetterTopic = "drpmlct-018-dlq-topic";
|
||||
MutableReactiveMessageConsumerSpec config = new MutableReactiveMessageConsumerSpec();
|
||||
config.setTopicNames(Collections.singletonList(topic));
|
||||
config.setSubscriptionName("drpmlct-sb-018");
|
||||
config.setNegativeAckRedeliveryDelay(Duration.ZERO);
|
||||
PulsarClient pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl())
|
||||
.build();
|
||||
ReactivePulsarClient reactivePulsarClient = AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
DefaultReactivePulsarConsumerFactory<String> pulsarConsumerFactory = new DefaultReactivePulsarConsumerFactory<>(
|
||||
reactivePulsarClient, config);
|
||||
ReactiveMessageConsumer<String> dlqConsumer = pulsarConsumerFactory.createConsumer(Schema.STRING,
|
||||
Collections.singletonList(b -> b.topicNames(Collections.singletonList(deadLetterTopic))));
|
||||
|
||||
// Ensure subscriptions are created
|
||||
pulsarConsumerFactory.createConsumer(Schema.STRING).consumeNothing().block(Duration.ofSeconds(10));
|
||||
dlqConsumer.consumeNothing().block(Duration.ofSeconds(10));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(6);
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = new ReactivePulsarContainerProperties<>();
|
||||
pulsarContainerProperties.setMessageHandler((ReactivePulsarStreamingHandler<String>) (msg) -> msg.map(m -> {
|
||||
latch.countDown();
|
||||
if (m.getValue().endsWith("4")) {
|
||||
return MessageResult.negativeAcknowledge(m.getMessageId());
|
||||
}
|
||||
return MessageResult.acknowledge(m.getMessageId());
|
||||
}));
|
||||
pulsarContainerProperties.setSchema(Schema.STRING);
|
||||
pulsarContainerProperties.setSubscriptionType(SubscriptionType.Shared);
|
||||
DefaultReactivePulsarMessageListenerContainer<String> container = new DefaultReactivePulsarMessageListenerContainer<>(
|
||||
pulsarConsumerFactory, pulsarContainerProperties);
|
||||
DeadLetterPolicy deadLetterPolicy = DeadLetterPolicy.builder().maxRedeliverCount(1)
|
||||
.deadLetterTopic(deadLetterTopic).build();
|
||||
container.setConsumerCustomizer(b -> b.deadLetterPolicy(deadLetterPolicy));
|
||||
container.start();
|
||||
MutableReactiveMessageSenderSpec prodConfig = new MutableReactiveMessageSenderSpec();
|
||||
prodConfig.setTopicName(topic);
|
||||
DefaultReactivePulsarSenderFactory<String> pulsarProducerFactory = new DefaultReactivePulsarSenderFactory<>(
|
||||
reactivePulsarClient, prodConfig, null);
|
||||
ReactivePulsarTemplate<String> pulsarTemplate = new ReactivePulsarTemplate<>(pulsarProducerFactory);
|
||||
Flux.range(0, 5).map(i -> "hello john doe" + i).as(pulsarTemplate::send).subscribe();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
CountDownLatch dlqLatch = new CountDownLatch(1);
|
||||
dlqConsumer.consumeOne(message -> {
|
||||
if (message.getValue().endsWith("4")) {
|
||||
dlqLatch.countDown();
|
||||
}
|
||||
return Mono.just(MessageResult.acknowledge(message.getMessageId()));
|
||||
}).block();
|
||||
|
||||
assertThat(dlqLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
container.stop();
|
||||
pulsarClient.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: person.proto
|
||||
|
||||
package org.springframework.pulsar.reactive.listener;
|
||||
|
||||
public final class Proto {
|
||||
|
||||
private Proto() {
|
||||
}
|
||||
|
||||
public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry) {
|
||||
}
|
||||
|
||||
public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry) {
|
||||
registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry);
|
||||
}
|
||||
|
||||
public interface PersonOrBuilder extends
|
||||
// @@protoc_insertion_point(interface_extends:proto.Person)
|
||||
com.google.protobuf.MessageOrBuilder {
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return Whether the id field is set.
|
||||
*/
|
||||
boolean hasId();
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return The id.
|
||||
*/
|
||||
int getId();
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return Whether the name field is set.
|
||||
*/
|
||||
boolean hasName();
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return The name.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return The bytes for name.
|
||||
*/
|
||||
com.google.protobuf.ByteString getNameBytes();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Protobuf type {@code proto.Person}
|
||||
*/
|
||||
public static final class Person extends com.google.protobuf.GeneratedMessageV3 implements
|
||||
// @@protoc_insertion_point(message_implements:proto.Person)
|
||||
PersonOrBuilder {
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
// Use Person.newBuilder() to construct.
|
||||
private Person(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {
|
||||
super(builder);
|
||||
}
|
||||
|
||||
private Person() {
|
||||
name_ = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unused" })
|
||||
protected Object newInstance(UnusedPrivateParameter unused) {
|
||||
return new Person();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final com.google.protobuf.UnknownFieldSet getUnknownFields() {
|
||||
return this.unknownFields;
|
||||
}
|
||||
|
||||
private Person(com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
this();
|
||||
if (extensionRegistry == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
int mutable_bitField0_ = 0;
|
||||
com.google.protobuf.UnknownFieldSet.Builder unknownFields = com.google.protobuf.UnknownFieldSet
|
||||
.newBuilder();
|
||||
try {
|
||||
boolean done = false;
|
||||
while (!done) {
|
||||
int tag = input.readTag();
|
||||
switch (tag) {
|
||||
case 0:
|
||||
done = true;
|
||||
break;
|
||||
case 8: {
|
||||
bitField0_ |= 0x00000001;
|
||||
id_ = input.readInt32();
|
||||
break;
|
||||
}
|
||||
case 18: {
|
||||
String s = input.readStringRequireUtf8();
|
||||
bitField0_ |= 0x00000002;
|
||||
name_ = s;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) {
|
||||
done = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
throw e.setUnfinishedMessage(this);
|
||||
}
|
||||
catch (com.google.protobuf.UninitializedMessageException e) {
|
||||
throw e.asInvalidProtocolBufferException().setUnfinishedMessage(this);
|
||||
}
|
||||
catch (java.io.IOException e) {
|
||||
throw new com.google.protobuf.InvalidProtocolBufferException(e).setUnfinishedMessage(this);
|
||||
}
|
||||
finally {
|
||||
this.unknownFields = unknownFields.build();
|
||||
makeExtensionsImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
|
||||
return Proto.internal_static_proto_Person_descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FieldAccessorTable internalGetFieldAccessorTable() {
|
||||
return Proto.internal_static_proto_Person_fieldAccessorTable.ensureFieldAccessorsInitialized(Person.class,
|
||||
Builder.class);
|
||||
}
|
||||
|
||||
private int bitField0_;
|
||||
|
||||
public static final int ID_FIELD_NUMBER = 1;
|
||||
|
||||
private int id_;
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return Whether the id field is set.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasId() {
|
||||
return ((bitField0_ & 0x00000001) != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return The id.
|
||||
*/
|
||||
@Override
|
||||
public int getId() {
|
||||
return id_;
|
||||
}
|
||||
|
||||
public static final int NAME_FIELD_NUMBER = 2;
|
||||
|
||||
private volatile Object name_;
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return Whether the name field is set.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasName() {
|
||||
return ((bitField0_ & 0x00000002) != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return The name.
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
Object ref = name_;
|
||||
if (ref instanceof String) {
|
||||
return (String) ref;
|
||||
}
|
||||
else {
|
||||
com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
|
||||
String s = bs.toStringUtf8();
|
||||
name_ = s;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return The bytes for name.
|
||||
*/
|
||||
@Override
|
||||
public com.google.protobuf.ByteString getNameBytes() {
|
||||
Object ref = name_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8((String) ref);
|
||||
name_ = b;
|
||||
return b;
|
||||
}
|
||||
else {
|
||||
return (com.google.protobuf.ByteString) ref;
|
||||
}
|
||||
}
|
||||
|
||||
private byte memoizedIsInitialized = -1;
|
||||
|
||||
@Override
|
||||
public final boolean isInitialized() {
|
||||
byte isInitialized = memoizedIsInitialized;
|
||||
if (isInitialized == 1)
|
||||
return true;
|
||||
if (isInitialized == 0)
|
||||
return false;
|
||||
|
||||
memoizedIsInitialized = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException {
|
||||
if (((bitField0_ & 0x00000001) != 0)) {
|
||||
output.writeInt32(1, id_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000002) != 0)) {
|
||||
com.google.protobuf.GeneratedMessageV3.writeString(output, 2, name_);
|
||||
}
|
||||
unknownFields.writeTo(output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSerializedSize() {
|
||||
int size = memoizedSize;
|
||||
if (size != -1)
|
||||
return size;
|
||||
|
||||
size = 0;
|
||||
if (((bitField0_ & 0x00000001) != 0)) {
|
||||
size += com.google.protobuf.CodedOutputStream.computeInt32Size(1, id_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000002) != 0)) {
|
||||
size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, name_);
|
||||
}
|
||||
size += unknownFields.getSerializedSize();
|
||||
memoizedSize = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof Person)) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
Person other = (Person) obj;
|
||||
|
||||
if (hasId() != other.hasId())
|
||||
return false;
|
||||
if (hasId()) {
|
||||
if (getId() != other.getId())
|
||||
return false;
|
||||
}
|
||||
if (hasName() != other.hasName())
|
||||
return false;
|
||||
if (hasName()) {
|
||||
if (!getName().equals(other.getName()))
|
||||
return false;
|
||||
}
|
||||
if (!unknownFields.equals(other.unknownFields))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (memoizedHashCode != 0) {
|
||||
return memoizedHashCode;
|
||||
}
|
||||
int hash = 41;
|
||||
hash = (19 * hash) + getDescriptor().hashCode();
|
||||
if (hasId()) {
|
||||
hash = (37 * hash) + ID_FIELD_NUMBER;
|
||||
hash = (53 * hash) + getId();
|
||||
}
|
||||
if (hasName()) {
|
||||
hash = (37 * hash) + NAME_FIELD_NUMBER;
|
||||
hash = (53 * hash) + getName().hashCode();
|
||||
}
|
||||
hash = (29 * hash) + unknownFields.hashCode();
|
||||
memoizedHashCode = hash;
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static Person parseFrom(java.nio.ByteBuffer data)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data);
|
||||
}
|
||||
|
||||
public static Person parseFrom(java.nio.ByteBuffer data,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data, extensionRegistry);
|
||||
}
|
||||
|
||||
public static Person parseFrom(com.google.protobuf.ByteString data)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data);
|
||||
}
|
||||
|
||||
public static Person parseFrom(com.google.protobuf.ByteString data,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data, extensionRegistry);
|
||||
}
|
||||
|
||||
public static Person parseFrom(byte[] data) throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data);
|
||||
}
|
||||
|
||||
public static Person parseFrom(byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return PARSER.parseFrom(data, extensionRegistry);
|
||||
}
|
||||
|
||||
public static Person parseFrom(java.io.InputStream input) throws java.io.IOException {
|
||||
return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input);
|
||||
}
|
||||
|
||||
public static Person parseFrom(java.io.InputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException {
|
||||
return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input, extensionRegistry);
|
||||
}
|
||||
|
||||
public static Person parseDelimitedFrom(java.io.InputStream input) throws java.io.IOException {
|
||||
return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input);
|
||||
}
|
||||
|
||||
public static Person parseDelimitedFrom(java.io.InputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException {
|
||||
return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input,
|
||||
extensionRegistry);
|
||||
}
|
||||
|
||||
public static Person parseFrom(com.google.protobuf.CodedInputStream input) throws java.io.IOException {
|
||||
return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input);
|
||||
}
|
||||
|
||||
public static Person parseFrom(com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException {
|
||||
return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input, extensionRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder newBuilderForType() {
|
||||
return newBuilder();
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return DEFAULT_INSTANCE.toBuilder();
|
||||
}
|
||||
|
||||
public static Builder newBuilder(Person prototype) {
|
||||
return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder toBuilder() {
|
||||
return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Builder newBuilderForType(BuilderParent parent) {
|
||||
Builder builder = new Builder(parent);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protobuf type {@code proto.Person}
|
||||
*/
|
||||
public static final class Builder extends com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
|
||||
// @@protoc_insertion_point(builder_implements:proto.Person)
|
||||
PersonOrBuilder {
|
||||
|
||||
public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
|
||||
return Proto.internal_static_proto_Person_descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FieldAccessorTable internalGetFieldAccessorTable() {
|
||||
return Proto.internal_static_proto_Person_fieldAccessorTable
|
||||
.ensureFieldAccessorsInitialized(Person.class, Builder.class);
|
||||
}
|
||||
|
||||
// Construct using
|
||||
// org.springframework.pulsar.listener.Proto.Person.newBuilder()
|
||||
private Builder() {
|
||||
maybeForceBuilderInitialization();
|
||||
}
|
||||
|
||||
private Builder(BuilderParent parent) {
|
||||
super(parent);
|
||||
maybeForceBuilderInitialization();
|
||||
}
|
||||
|
||||
private void maybeForceBuilderInitialization() {
|
||||
if (com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder clear() {
|
||||
super.clear();
|
||||
id_ = 0;
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
name_ = "";
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() {
|
||||
return Proto.internal_static_proto_Person_descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person getDefaultInstanceForType() {
|
||||
return Person.getDefaultInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person build() {
|
||||
Person result = buildPartial();
|
||||
if (!result.isInitialized()) {
|
||||
throw newUninitializedMessageException(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person buildPartial() {
|
||||
Person result = new Person(this);
|
||||
int from_bitField0_ = bitField0_;
|
||||
int to_bitField0_ = 0;
|
||||
if (((from_bitField0_ & 0x00000001) != 0)) {
|
||||
result.id_ = id_;
|
||||
to_bitField0_ |= 0x00000001;
|
||||
}
|
||||
if (((from_bitField0_ & 0x00000002) != 0)) {
|
||||
to_bitField0_ |= 0x00000002;
|
||||
}
|
||||
result.name_ = name_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder clone() {
|
||||
return super.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setField(com.google.protobuf.Descriptors.FieldDescriptor field, Object value) {
|
||||
return super.setField(field, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) {
|
||||
return super.clearField(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) {
|
||||
return super.clearOneof(oneof);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder setRepeatedField(com.google.protobuf.Descriptors.FieldDescriptor field, int index,
|
||||
Object value) {
|
||||
return super.setRepeatedField(field, index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder addRepeatedField(com.google.protobuf.Descriptors.FieldDescriptor field, Object value) {
|
||||
return super.addRepeatedField(field, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mergeFrom(com.google.protobuf.Message other) {
|
||||
if (other instanceof Person) {
|
||||
return mergeFrom((Person) other);
|
||||
}
|
||||
else {
|
||||
super.mergeFrom(other);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public Builder mergeFrom(Person other) {
|
||||
if (other == Person.getDefaultInstance())
|
||||
return this;
|
||||
if (other.hasId()) {
|
||||
setId(other.getId());
|
||||
}
|
||||
if (other.hasName()) {
|
||||
bitField0_ |= 0x00000002;
|
||||
name_ = other.name_;
|
||||
onChanged();
|
||||
}
|
||||
this.mergeUnknownFields(other.unknownFields);
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isInitialized() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder mergeFrom(com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException {
|
||||
Person parsedMessage = null;
|
||||
try {
|
||||
parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
|
||||
}
|
||||
catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
parsedMessage = (Person) e.getUnfinishedMessage();
|
||||
throw e.unwrapIOException();
|
||||
}
|
||||
finally {
|
||||
if (parsedMessage != null) {
|
||||
mergeFrom(parsedMessage);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private int bitField0_;
|
||||
|
||||
private int id_;
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return Whether the id field is set.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasId() {
|
||||
return ((bitField0_ & 0x00000001) != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return The id.
|
||||
*/
|
||||
@Override
|
||||
public int getId() {
|
||||
return id_;
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @param value The id to set.
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder setId(int value) {
|
||||
bitField0_ |= 0x00000001;
|
||||
id_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional int32 id = 1;</code>
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder clearId() {
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
id_ = 0;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
private Object name_ = "";
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return Whether the name field is set.
|
||||
*/
|
||||
public boolean hasName() {
|
||||
return ((bitField0_ & 0x00000002) != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return The name.
|
||||
*/
|
||||
public String getName() {
|
||||
Object ref = name_;
|
||||
if (!(ref instanceof String)) {
|
||||
com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
|
||||
String s = bs.toStringUtf8();
|
||||
name_ = s;
|
||||
return s;
|
||||
}
|
||||
else {
|
||||
return (String) ref;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return The bytes for name.
|
||||
*/
|
||||
public com.google.protobuf.ByteString getNameBytes() {
|
||||
Object ref = name_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b = com.google.protobuf.ByteString.copyFromUtf8((String) ref);
|
||||
name_ = b;
|
||||
return b;
|
||||
}
|
||||
else {
|
||||
return (com.google.protobuf.ByteString) ref;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @param value The name to set.
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder setName(String value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000002;
|
||||
name_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder clearName() {
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
name_ = getDefaultInstance().getName();
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>optional string name = 2;</code>
|
||||
* @param value The bytes for name to set.
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder setNameBytes(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
checkByteStringIsUtf8(value);
|
||||
bitField0_ |= 0x00000002;
|
||||
name_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Builder setUnknownFields(final com.google.protobuf.UnknownFieldSet unknownFields) {
|
||||
return super.setUnknownFields(unknownFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Builder mergeUnknownFields(final com.google.protobuf.UnknownFieldSet unknownFields) {
|
||||
return super.mergeUnknownFields(unknownFields);
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:proto.Person)
|
||||
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(class_scope:proto.Person)
|
||||
private static final Person DEFAULT_INSTANCE;
|
||||
static {
|
||||
DEFAULT_INSTANCE = new Person();
|
||||
}
|
||||
|
||||
public static Person getDefaultInstance() {
|
||||
return DEFAULT_INSTANCE;
|
||||
}
|
||||
|
||||
private static final com.google.protobuf.Parser<Person> PARSER = new com.google.protobuf.AbstractParser<Person>() {
|
||||
@Override
|
||||
public Person parsePartialFrom(com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return new Person(input, extensionRegistry);
|
||||
}
|
||||
};
|
||||
|
||||
public static com.google.protobuf.Parser<Person> parser() {
|
||||
return PARSER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.google.protobuf.Parser<Person> getParserForType() {
|
||||
return PARSER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person getDefaultInstanceForType() {
|
||||
return DEFAULT_INSTANCE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final com.google.protobuf.Descriptors.Descriptor internal_static_proto_Person_descriptor;
|
||||
|
||||
private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable internal_static_proto_Person_fieldAccessorTable;
|
||||
|
||||
public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private static com.google.protobuf.Descriptors.FileDescriptor descriptor;
|
||||
static {
|
||||
String[] descriptorData = { "\n\014person.proto\022\005proto\"<\n\006Person\022\017\n\002id\030\001 "
|
||||
+ "\001(\005H\000\210\001\001\022\021\n\004name\030\002 \001(\tH\001\210\001\001B\005\n\003_idB\007\n\005_n"
|
||||
+ "ameB,\n#org.springframework.pulsar.listen" + "erB\005Protob\006proto3" };
|
||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(descriptorData,
|
||||
new com.google.protobuf.Descriptors.FileDescriptor[] {});
|
||||
internal_static_proto_Person_descriptor = getDescriptor().getMessageTypes().get(0);
|
||||
internal_static_proto_Person_fieldAccessorTable = new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
|
||||
internal_static_proto_Person_descriptor, new String[] { "Id", "Name", "Id", "Name", });
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(outer_class_scope)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
/*
|
||||
* 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.reactive.listener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.pulsar.client.admin.PulsarAdmin;
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.MessageId;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.client.impl.schema.AvroSchema;
|
||||
import org.apache.pulsar.client.impl.schema.JSONSchema;
|
||||
import org.apache.pulsar.client.impl.schema.ProtobufSchema;
|
||||
import org.apache.pulsar.common.schema.KeyValue;
|
||||
import org.apache.pulsar.common.schema.KeyValueEncodingType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.api.MessageResult;
|
||||
import org.apache.pulsar.reactive.client.api.MutableReactiveMessageConsumerSpec;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.pulsar.config.PulsarClientConfiguration;
|
||||
import org.springframework.pulsar.config.PulsarClientFactoryBean;
|
||||
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTestContainerSupport;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpointRegistry;
|
||||
import org.springframework.pulsar.reactive.config.annotation.EnableReactivePulsar;
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.support.PulsarHeaders;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactivePulsarListener} annotation.
|
||||
*
|
||||
* @author Christophe Bornet
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
public class ReactivePulsarListenerTests implements PulsarTestContainerSupport {
|
||||
|
||||
@Autowired
|
||||
PulsarTemplate<String> pulsarTemplate;
|
||||
|
||||
@Autowired
|
||||
private PulsarClient pulsarClient;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableReactivePulsar
|
||||
public static class TopLevelConfig {
|
||||
|
||||
@Bean
|
||||
public PulsarProducerFactory<String> pulsarProducerFactory(PulsarClient pulsarClient) {
|
||||
return new DefaultPulsarProducerFactory<>(pulsarClient, new HashMap<>());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PulsarClientFactoryBean pulsarClientFactoryBean(PulsarClientConfiguration pulsarClientConfiguration) {
|
||||
return new PulsarClientFactoryBean(pulsarClientConfiguration);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PulsarClientConfiguration pulsarClientConfiguration() {
|
||||
return new PulsarClientConfiguration(Map.of("serviceUrl", PulsarTestContainerSupport.getPulsarBrokerUrl()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReactivePulsarClient pulsarReactivePulsarClient(PulsarClient pulsarClient) {
|
||||
return AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PulsarTemplate<String> pulsarTemplate(PulsarProducerFactory<String> pulsarProducerFactory) {
|
||||
return new PulsarTemplate<>(pulsarProducerFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReactivePulsarConsumerFactory<String> pulsarConsumerFactory(ReactivePulsarClient pulsarClient) {
|
||||
return new DefaultReactivePulsarConsumerFactory<>(pulsarClient, new MutableReactiveMessageConsumerSpec());
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactivePulsarListenerContainerFactory<String> reactivePulsarListenerContainerFactory(
|
||||
ReactivePulsarConsumerFactory<String> pulsarConsumerFactory) {
|
||||
return new DefaultReactivePulsarListenerContainerFactory<>(pulsarConsumerFactory,
|
||||
new ReactivePulsarContainerProperties<>());
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarAdministration pulsarAdministration() {
|
||||
return new PulsarAdministration(
|
||||
PulsarAdmin.builder().serviceHttpUrl(PulsarTestContainerSupport.getHttpServiceUrl()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
PulsarTopic partitionedTopic() {
|
||||
return PulsarTopic.builder("persistent://public/default/concurrency-on-pl").numberOfPartitions(3).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ContextConfiguration(classes = PulsarListenerBasicTestCases.TestPulsarListenersForBasicScenario.class)
|
||||
class PulsarListenerBasicTestCases {
|
||||
|
||||
static CountDownLatch latch1 = new CountDownLatch(1);
|
||||
static CountDownLatch latch2 = new CountDownLatch(1);
|
||||
static CountDownLatch latch3 = new CountDownLatch(3);
|
||||
|
||||
@Autowired
|
||||
ReactivePulsarListenerEndpointRegistry<String> registry;
|
||||
|
||||
@Test
|
||||
void testPulsarListener() throws Exception {
|
||||
ReactivePulsarContainerProperties<String> pulsarContainerProperties = registry.getListenerContainer("id-1")
|
||||
.getContainerProperties();
|
||||
assertThat(pulsarContainerProperties.getTopics()).containsExactly("topic-1");
|
||||
assertThat(pulsarContainerProperties.getSubscriptionName()).isEqualTo("subscription-1");
|
||||
pulsarTemplate.send("topic-1", "hello foo");
|
||||
assertThat(latch1.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPulsarListenerWithConsumerCustomizer() throws Exception {
|
||||
pulsarTemplate.send("topic-2", "hello foo");
|
||||
assertThat(latch2.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPulsarListenerWithTopicsPattern() throws Exception {
|
||||
ReactivePulsarContainerProperties<String> containerProperties = registry.getListenerContainer("id-3")
|
||||
.getContainerProperties();
|
||||
assertThat(containerProperties.getTopicsPattern().toString())
|
||||
.isEqualTo("persistent://public/default/pattern.*");
|
||||
|
||||
pulsarTemplate.send("persistent://public/default/pattern-1", "hello baz");
|
||||
pulsarTemplate.send("persistent://public/default/pattern-2", "hello baz");
|
||||
pulsarTemplate.send("persistent://public/default/pattern-3", "hello baz");
|
||||
|
||||
assertThat(latch3.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@EnableReactivePulsar
|
||||
@Configuration
|
||||
static class TestPulsarListenersForBasicScenario {
|
||||
|
||||
@ReactivePulsarListener(id = "id-1", topics = "topic-1", subscriptionName = "subscription-1",
|
||||
consumerCustomizer = "consumerCustomizer")
|
||||
Mono<Void> listen1(String message) {
|
||||
latch1.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(consumerCustomizer = "listen2Customizer")
|
||||
Mono<Void> listen2(String message) {
|
||||
latch2.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<String> listen2Customizer() {
|
||||
return b -> b.topicNames(List.of("topic-2"))
|
||||
.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(id = "id-3", topicPattern = "persistent://public/default/pattern.*",
|
||||
subscriptionName = "subscription-3", consumerCustomizer = "consumerCustomizer")
|
||||
Mono<Void> listen3(String message) {
|
||||
latch3.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<String> consumerCustomizer() {
|
||||
return b -> b.topicsPatternAutoDiscoveryPeriod(Duration.ofSeconds(2))
|
||||
.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ContextConfiguration(classes = PulsarListenerStreamingTestCases.TestPulsarListenersForStreaming.class)
|
||||
class PulsarListenerStreamingTestCases {
|
||||
|
||||
static CountDownLatch latch1 = new CountDownLatch(10);
|
||||
static CountDownLatch latch2 = new CountDownLatch(10);
|
||||
|
||||
@Test
|
||||
void testPulsarListenerStreaming() throws Exception {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
pulsarTemplate.send("streaming-1", "hello foo");
|
||||
}
|
||||
assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPulsarListenerStreamingSpringMessage() throws Exception {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
pulsarTemplate.send("streaming-2", "hello foo");
|
||||
}
|
||||
assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@EnableReactivePulsar
|
||||
@Configuration
|
||||
static class TestPulsarListenersForStreaming {
|
||||
|
||||
@ReactivePulsarListener(topics = "streaming-1", stream = true, consumerCustomizer = "consumerCustomizer")
|
||||
Flux<MessageResult<Void>> listen1(Flux<Message<String>> messages) {
|
||||
return messages.doOnNext(m -> latch1.countDown()).map(m -> MessageResult.acknowledge(m.getMessageId()));
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(topics = "streaming-2", stream = true, consumerCustomizer = "consumerCustomizer")
|
||||
Flux<MessageResult<Void>> listen2(Flux<org.springframework.messaging.Message<String>> messages) {
|
||||
return messages.doOnNext(m -> latch2.countDown()).map(m -> {
|
||||
Object mId = m.getHeaders().get(PulsarHeaders.MESSAGE_ID);
|
||||
if (mId instanceof MessageId) {
|
||||
return (MessageId) mId;
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("Missing message Id");
|
||||
}
|
||||
}).map(MessageResult::acknowledge);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<String> consumerCustomizer() {
|
||||
return b -> b.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ContextConfiguration(classes = DeadLetterPolicyTest.DeadLetterPolicyConfig.class)
|
||||
class DeadLetterPolicyTest {
|
||||
|
||||
private static CountDownLatch latch = new CountDownLatch(2);
|
||||
|
||||
private static CountDownLatch dlqLatch = new CountDownLatch(1);
|
||||
|
||||
@Test
|
||||
void pulsarListenerWithDeadLetterPolicy() throws Exception {
|
||||
pulsarTemplate.send("dlpt-topic-1", "hello");
|
||||
assertThat(dlqLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@EnableReactivePulsar
|
||||
@Configuration
|
||||
static class DeadLetterPolicyConfig {
|
||||
|
||||
@ReactivePulsarListener(id = "deadLetterPolicyListener", subscriptionName = "deadLetterPolicySubscription",
|
||||
topics = "dlpt-topic-1", deadLetterPolicy = "deadLetterPolicy",
|
||||
consumerCustomizer = "consumerCustomizer", subscriptionType = SubscriptionType.Shared)
|
||||
Mono<Void> listen(String msg) {
|
||||
latch.countDown();
|
||||
return Mono.error(new RuntimeException("fail " + msg));
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(id = "dlqListener", topics = "dlpt-dlq-topic",
|
||||
consumerCustomizer = "consumerCustomizer")
|
||||
Mono<Void> listenDlq(String msg) {
|
||||
dlqLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
DeadLetterPolicy deadLetterPolicy() {
|
||||
return DeadLetterPolicy.builder().maxRedeliverCount(1).deadLetterTopic("dlpt-dlq-topic").build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<String> consumerCustomizer() {
|
||||
return b -> b.negativeAckRedeliveryDelay(Duration.ofSeconds(1))
|
||||
.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ContextConfiguration(classes = SchemaTestCases.SchemaTestConfig.class)
|
||||
class SchemaTestCases {
|
||||
|
||||
static CountDownLatch jsonLatch = new CountDownLatch(3);
|
||||
static CountDownLatch avroLatch = new CountDownLatch(3);
|
||||
static CountDownLatch keyvalueLatch = new CountDownLatch(3);
|
||||
static CountDownLatch protobufLatch = new CountDownLatch(3);
|
||||
|
||||
@Test
|
||||
void jsonSchema() throws Exception {
|
||||
PulsarProducerFactory<User> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(pulsarClient,
|
||||
Collections.emptyMap());
|
||||
PulsarTemplate<User> template = new PulsarTemplate<>(pulsarProducerFactory);
|
||||
template.setSchema(JSONSchema.of(User.class));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
template.send("json-topic", new User("Jason", i));
|
||||
}
|
||||
assertThat(jsonLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void avroSchema() throws Exception {
|
||||
PulsarProducerFactory<User> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(pulsarClient,
|
||||
Collections.emptyMap());
|
||||
PulsarTemplate<User> template = new PulsarTemplate<>(pulsarProducerFactory);
|
||||
template.setSchema(AvroSchema.of(User.class));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
template.send("avro-topic", new User("Avi", i));
|
||||
}
|
||||
assertThat(avroLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyvalueSchema() throws Exception {
|
||||
PulsarProducerFactory<KeyValue<String, Integer>> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(
|
||||
pulsarClient, Collections.emptyMap());
|
||||
PulsarTemplate<KeyValue<String, Integer>> template = new PulsarTemplate<>(pulsarProducerFactory);
|
||||
Schema<KeyValue<String, Integer>> kvSchema = Schema.KeyValue(Schema.STRING, Schema.INT32,
|
||||
KeyValueEncodingType.INLINE);
|
||||
template.setSchema(kvSchema);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
template.send("keyvalue-topic", new KeyValue<>("Kevin", i));
|
||||
}
|
||||
assertThat(keyvalueLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void protobufSchema() throws Exception {
|
||||
PulsarProducerFactory<Proto.Person> pulsarProducerFactory = new DefaultPulsarProducerFactory<>(pulsarClient,
|
||||
Collections.emptyMap());
|
||||
PulsarTemplate<Proto.Person> template = new PulsarTemplate<>(pulsarProducerFactory);
|
||||
template.setSchema(ProtobufSchema.of(Proto.Person.class));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
template.send("protobuf-topic", Proto.Person.newBuilder().setId(i).setName("Paul").build());
|
||||
}
|
||||
assertThat(protobufLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@EnableReactivePulsar
|
||||
@Configuration
|
||||
static class SchemaTestConfig {
|
||||
|
||||
@ReactivePulsarListener(id = "jsonListener", topics = "json-topic", schemaType = SchemaType.JSON,
|
||||
consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> listenJson(User message) {
|
||||
jsonLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(id = "avroListener", topics = "avro-topic", schemaType = SchemaType.AVRO,
|
||||
consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> listenAvro(User message) {
|
||||
avroLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(id = "keyvalueListener", topics = "keyvalue-topic",
|
||||
schemaType = SchemaType.KEY_VALUE, consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> listenKeyvalue(KeyValue<String, Integer> message) {
|
||||
keyvalueLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(id = "protobufListener", topics = "protobuf-topic",
|
||||
schemaType = SchemaType.PROTOBUF, consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> listenProtobuf(Proto.Person message) {
|
||||
protobufLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<?> subscriptionInitialPositionEarliest() {
|
||||
return b -> b.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class User {
|
||||
|
||||
private String name;
|
||||
|
||||
private int age;
|
||||
|
||||
User() {
|
||||
|
||||
}
|
||||
|
||||
User(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
User user = (User) o;
|
||||
return age == user.age && Objects.equals(name, user.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, age);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ContextConfiguration(classes = ReactivePulsarListenerTests.PulsarHeadersTest.PulsarListenerWithHeadersConfig.class)
|
||||
class PulsarHeadersTest {
|
||||
|
||||
static CountDownLatch simpleListenerLatch = new CountDownLatch(1);
|
||||
static CountDownLatch pulsarMessageListenerLatch = new CountDownLatch(1);
|
||||
static CountDownLatch springMessagingMessageListenerLatch = new CountDownLatch(1);
|
||||
|
||||
static AtomicReference<String> capturedData = new AtomicReference<>();
|
||||
static AtomicReference<MessageId> messageId = new AtomicReference<>();
|
||||
static AtomicReference<String> topicName = new AtomicReference<>();
|
||||
static AtomicReference<String> fooValue = new AtomicReference<>();
|
||||
static AtomicReference<byte[]> rawData = new AtomicReference<>();
|
||||
|
||||
@Test
|
||||
void simpleListenerWithHeaders() throws Exception {
|
||||
MessageId messageId = pulsarTemplate.newMessage("hello-simple-listener")
|
||||
.withMessageCustomizer(
|
||||
messageBuilder -> messageBuilder.property("foo", "simpleListenerWithHeaders"))
|
||||
.withTopic("simpleListenerWithHeaders").send();
|
||||
assertThat(simpleListenerLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(capturedData.get()).isEqualTo("hello-simple-listener");
|
||||
assertThat(PulsarHeadersTest.messageId.get()).isEqualTo(messageId);
|
||||
assertThat(topicName.get()).isEqualTo("persistent://public/default/simpleListenerWithHeaders");
|
||||
assertThat(fooValue.get()).isEqualTo("simpleListenerWithHeaders");
|
||||
assertThat(rawData.get()).isEqualTo("hello-simple-listener".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pulsarMessageListenerWithHeaders() throws Exception {
|
||||
MessageId messageId = pulsarTemplate.newMessage("hello-pulsar-message-listener")
|
||||
.withMessageCustomizer(
|
||||
messageBuilder -> messageBuilder.property("foo", "pulsarMessageListenerWithHeaders"))
|
||||
.withTopic("pulsarMessageListenerWithHeaders").send();
|
||||
assertThat(pulsarMessageListenerLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(capturedData.get()).isEqualTo("hello-pulsar-message-listener");
|
||||
assertThat(PulsarHeadersTest.messageId.get()).isEqualTo(messageId);
|
||||
assertThat(topicName.get()).isEqualTo("persistent://public/default/pulsarMessageListenerWithHeaders");
|
||||
assertThat(fooValue.get()).isEqualTo("pulsarMessageListenerWithHeaders");
|
||||
assertThat(rawData.get()).isEqualTo("hello-pulsar-message-listener".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void springMessagingMessageListenerWithHeaders() throws Exception {
|
||||
MessageId messageId = pulsarTemplate.newMessage("hello-spring-messaging-message-listener")
|
||||
.withMessageCustomizer(messageBuilder -> messageBuilder.property("foo",
|
||||
"springMessagingMessageListenerWithHeaders"))
|
||||
.withTopic("springMessagingMessageListenerWithHeaders").send();
|
||||
assertThat(springMessagingMessageListenerLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(capturedData.get()).isEqualTo("hello-spring-messaging-message-listener");
|
||||
assertThat(PulsarHeadersTest.messageId.get()).isEqualTo(messageId);
|
||||
assertThat(topicName.get())
|
||||
.isEqualTo("persistent://public/default/springMessagingMessageListenerWithHeaders");
|
||||
assertThat(fooValue.get()).isEqualTo("springMessagingMessageListenerWithHeaders");
|
||||
assertThat(rawData.get())
|
||||
.isEqualTo("hello-spring-messaging-message-listener".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@EnableReactivePulsar
|
||||
@Configuration
|
||||
static class PulsarListenerWithHeadersConfig {
|
||||
|
||||
@ReactivePulsarListener(subscriptionName = "simple-listener-with-headers-sub",
|
||||
topics = "simpleListenerWithHeaders", consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> simpleListenerWithHeaders(String data, @Header(PulsarHeaders.MESSAGE_ID) MessageId messageId,
|
||||
@Header(PulsarHeaders.TOPIC_NAME) String topicName, @Header(PulsarHeaders.RAW_DATA) byte[] rawData,
|
||||
@Header("foo") String foo) {
|
||||
capturedData.set(data);
|
||||
PulsarHeadersTest.messageId.set(messageId);
|
||||
PulsarHeadersTest.topicName.set(topicName);
|
||||
fooValue.set(foo);
|
||||
PulsarHeadersTest.rawData.set(rawData);
|
||||
simpleListenerLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(subscriptionName = "pulsar-message-listener-with-headers-sub",
|
||||
topics = "pulsarMessageListenerWithHeaders",
|
||||
consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> pulsarMessageListenerWithHeaders(Message<String> data,
|
||||
@Header(PulsarHeaders.MESSAGE_ID) MessageId messageId,
|
||||
@Header(PulsarHeaders.TOPIC_NAME) String topicName, @Header(PulsarHeaders.RAW_DATA) byte[] rawData,
|
||||
@Header("foo") String foo) {
|
||||
capturedData.set(data.getValue());
|
||||
PulsarHeadersTest.messageId.set(messageId);
|
||||
PulsarHeadersTest.topicName.set(topicName);
|
||||
fooValue.set(foo);
|
||||
PulsarHeadersTest.rawData.set(rawData);
|
||||
pulsarMessageListenerLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(subscriptionName = "pulsar-message-listener-with-headers-sub",
|
||||
topics = "springMessagingMessageListenerWithHeaders",
|
||||
consumerCustomizer = "subscriptionInitialPositionEarliest")
|
||||
Mono<Void> springMessagingMessageListenerWithHeaders(org.springframework.messaging.Message<String> data,
|
||||
@Header(PulsarHeaders.MESSAGE_ID) MessageId messageId,
|
||||
@Header(PulsarHeaders.RAW_DATA) byte[] rawData, @Header(PulsarHeaders.TOPIC_NAME) String topicName,
|
||||
@Header("foo") String foo) {
|
||||
capturedData.set(data.getPayload());
|
||||
PulsarHeadersTest.messageId.set(messageId);
|
||||
PulsarHeadersTest.topicName.set(topicName);
|
||||
fooValue.set(foo);
|
||||
PulsarHeadersTest.rawData.set(rawData);
|
||||
springMessagingMessageListenerLatch.countDown();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<?> subscriptionInitialPositionEarliest() {
|
||||
return b -> b.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ContextConfiguration(classes = PulsarListenerConcurrencyTestCases.TestPulsarListenersForConcurrency.class)
|
||||
class PulsarListenerConcurrencyTestCases {
|
||||
|
||||
static CountDownLatch latch = new CountDownLatch(100);
|
||||
|
||||
static BlockingQueue<String> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
@Test
|
||||
void pulsarListenerWithConcurrency() throws Exception {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
pulsarTemplate.send("pulsarListenerConcurrency", "hello foo");
|
||||
}
|
||||
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void pulsarListenerWithConcurrencyKeyOrdered() throws Exception {
|
||||
pulsarTemplate.newMessage("first").withTopic("pulsarListenerWithConcurrencyKeyOrdered")
|
||||
.withMessageCustomizer(m -> m.key("key")).send();
|
||||
pulsarTemplate.newMessage("second").withTopic("pulsarListenerWithConcurrencyKeyOrdered")
|
||||
.withMessageCustomizer(m -> m.key("key")).send();
|
||||
assertThat(queue.poll(5, TimeUnit.SECONDS)).isEqualTo("first");
|
||||
assertThat(queue.poll(5, TimeUnit.SECONDS)).isEqualTo("second");
|
||||
}
|
||||
|
||||
@EnableReactivePulsar
|
||||
@Configuration
|
||||
static class TestPulsarListenersForConcurrency {
|
||||
|
||||
@ReactivePulsarListener(topics = "pulsarListenerConcurrency", consumerCustomizer = "consumerCustomizer",
|
||||
concurrency = "100")
|
||||
Mono<Void> listen1(String message) {
|
||||
latch.countDown();
|
||||
// if messages are not handled concurrently, this will make the latch
|
||||
// await timeout.
|
||||
return Mono.delay(Duration.ofMillis(100)).then();
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(topics = "pulsarListenerWithConcurrencyKeyOrdered",
|
||||
consumerCustomizer = "consumerCustomizer", concurrency = "100", useKeyOrderedProcessing = "true")
|
||||
Mono<Void> listen2(String message) {
|
||||
if (message.equals("first")) {
|
||||
// if message processing is not ordered by keys, "first" will be added
|
||||
// to the queue after "second"
|
||||
return Mono.delay(Duration.ofMillis(1000)).doOnNext(m -> queue.add(message)).then();
|
||||
}
|
||||
queue.add(message);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveMessageConsumerBuilderCustomizer<String> consumerCustomizer() {
|
||||
return b -> b.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
spring-pulsar-reactive/src/test/resources/logback-test.xml
Normal file
12
spring-pulsar-reactive/src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="org.testcontainers" level="ERROR"/>
|
||||
<logger name="com.github.dockerjava" level="ERROR"/>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user