Split reactive into its own module

Remove redundant dep
This commit is contained in:
Chris Bono
2022-11-17 18:21:44 -06:00
committed by Soby Chacko
parent 0a6808441b
commit a8d65ec168
64 changed files with 1335 additions and 161 deletions

View 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
}

View File

@@ -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"));
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>> {
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

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

View File

@@ -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));
}
}
}

View File

@@ -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 "";
}

View File

@@ -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>> {
}
}

View File

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

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"));
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

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

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.pulsar.reactive.aot.ReactivePulsarRuntimeHints

View File

@@ -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");
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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();
}
}

View File

@@ -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)
}

View File

@@ -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);
}
}
}
}

View 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>