Merge pull request #34763 from onobc
* pr/34763: Add support for Apache Pulsar Closes gh-34763pull/37196/head
commit
59e591c13c
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy.DeadLetterPolicyBuilder;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Helper class used to map {@link PulsarProperties.Consumer.DeadLetterPolicy dead letter
|
||||
* policy properties}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class DeadLetterPolicyMapper {
|
||||
|
||||
private DeadLetterPolicyMapper() {
|
||||
}
|
||||
|
||||
static DeadLetterPolicy map(PulsarProperties.Consumer.DeadLetterPolicy policy) {
|
||||
Assert.state(policy.getMaxRedeliverCount() > 0,
|
||||
"Pulsar DeadLetterPolicy must have a positive 'max-redelivery-count' property value");
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
DeadLetterPolicyBuilder builder = DeadLetterPolicy.builder();
|
||||
map.from(policy::getMaxRedeliverCount).to(builder::maxRedeliverCount);
|
||||
map.from(policy::getRetryLetterTopic).to(builder::retryLetterTopic);
|
||||
map.from(policy::getDeadLetterTopic).to(builder::deadLetterTopic);
|
||||
map.from(policy::getInitialSubscriptionName).to(builder::initialSubscriptionName);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.ConsumerBuilder;
|
||||
import org.apache.pulsar.client.api.ProducerBuilder;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.ReaderBuilder;
|
||||
import org.apache.pulsar.client.api.interceptor.ProducerInterceptor;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.pulsar.annotation.EnablePulsar;
|
||||
import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory;
|
||||
import org.springframework.pulsar.config.PulsarAnnotationSupportBeanNames;
|
||||
import org.springframework.pulsar.core.CachingPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.ConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarReaderFactory;
|
||||
import org.springframework.pulsar.core.ProducerBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.PulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.PulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarReaderFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.ReaderBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
import org.springframework.pulsar.listener.PulsarContainerProperties;
|
||||
import org.springframework.pulsar.reader.PulsarReaderContainerProperties;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Apache Pulsar.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Soby Chacko
|
||||
* @author Alexander Preuß
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ PulsarClient.class, PulsarTemplate.class })
|
||||
@Import(PulsarConfiguration.class)
|
||||
public class PulsarAutoConfiguration {
|
||||
|
||||
private PulsarProperties properties;
|
||||
|
||||
private PulsarPropertiesMapper propertiesMapper;
|
||||
|
||||
PulsarAutoConfiguration(PulsarProperties properties) {
|
||||
this.properties = properties;
|
||||
this.propertiesMapper = new PulsarPropertiesMapper(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PulsarProducerFactory.class)
|
||||
@ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "false")
|
||||
DefaultPulsarProducerFactory<?> pulsarProducerFactory(PulsarClient pulsarClient, TopicResolver topicResolver,
|
||||
ObjectProvider<ProducerBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ProducerBuilderCustomizer<Object>> lambdaSafeCustomizers = lambdaSafeProducerBuilderCustomizers(
|
||||
customizersProvider);
|
||||
return new DefaultPulsarProducerFactory<>(pulsarClient, this.properties.getProducer().getTopicName(),
|
||||
lambdaSafeCustomizers, topicResolver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PulsarProducerFactory.class)
|
||||
@ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true)
|
||||
CachingPulsarProducerFactory<?> cachingPulsarProducerFactory(PulsarClient pulsarClient, TopicResolver topicResolver,
|
||||
ObjectProvider<ProducerBuilderCustomizer<?>> customizersProvider) {
|
||||
PulsarProperties.Producer.Cache cacheProperties = this.properties.getProducer().getCache();
|
||||
List<ProducerBuilderCustomizer<Object>> lambdaSafeCustomizers = lambdaSafeProducerBuilderCustomizers(
|
||||
customizersProvider);
|
||||
return new CachingPulsarProducerFactory<>(pulsarClient, this.properties.getProducer().getTopicName(),
|
||||
lambdaSafeCustomizers, topicResolver, cacheProperties.getExpireAfterAccess(),
|
||||
cacheProperties.getMaximumSize(), cacheProperties.getInitialCapacity());
|
||||
}
|
||||
|
||||
private List<ProducerBuilderCustomizer<Object>> lambdaSafeProducerBuilderCustomizers(
|
||||
ObjectProvider<ProducerBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ProducerBuilderCustomizer<?>> customizers = new ArrayList<>();
|
||||
customizers.add(this.propertiesMapper::customizeProducerBuilder);
|
||||
customizers.addAll(customizersProvider.orderedStream().toList());
|
||||
return List.of((builder) -> applyProducerBuilderCustomizers(customizers, builder));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyProducerBuilderCustomizers(List<ProducerBuilderCustomizer<?>> customizers,
|
||||
ProducerBuilder<?> builder) {
|
||||
LambdaSafe.callbacks(ProducerBuilderCustomizer.class, customizers, builder)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
PulsarTemplate<?> pulsarTemplate(PulsarProducerFactory<?> pulsarProducerFactory,
|
||||
ObjectProvider<ProducerInterceptor> producerInterceptors, SchemaResolver schemaResolver,
|
||||
TopicResolver topicResolver) {
|
||||
return new PulsarTemplate<>(pulsarProducerFactory, producerInterceptors.orderedStream().toList(),
|
||||
schemaResolver, topicResolver, this.properties.getTemplate().isObservationsEnabled());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PulsarConsumerFactory.class)
|
||||
DefaultPulsarConsumerFactory<Object> pulsarConsumerFactory(PulsarClient pulsarClient,
|
||||
ObjectProvider<ConsumerBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ConsumerBuilderCustomizer<?>> customizers = new ArrayList<>();
|
||||
customizers.add(this.propertiesMapper::customizeConsumerBuilder);
|
||||
customizers.addAll(customizersProvider.orderedStream().toList());
|
||||
List<ConsumerBuilderCustomizer<Object>> lambdaSafeCustomizers = List
|
||||
.of((builder) -> applyConsumerBuilderCustomizers(customizers, builder));
|
||||
return new DefaultPulsarConsumerFactory<>(pulsarClient, lambdaSafeCustomizers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyConsumerBuilderCustomizers(List<ConsumerBuilderCustomizer<?>> customizers,
|
||||
ConsumerBuilder<?> builder) {
|
||||
LambdaSafe.callbacks(ConsumerBuilderCustomizer.class, customizers, builder)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "pulsarListenerContainerFactory")
|
||||
ConcurrentPulsarListenerContainerFactory<Object> pulsarListenerContainerFactory(
|
||||
PulsarConsumerFactory<Object> pulsarConsumerFactory, SchemaResolver schemaResolver,
|
||||
TopicResolver topicResolver) {
|
||||
PulsarContainerProperties containerProperties = new PulsarContainerProperties();
|
||||
containerProperties.setSchemaResolver(schemaResolver);
|
||||
containerProperties.setTopicResolver(topicResolver);
|
||||
this.propertiesMapper.customizeContainerProperties(containerProperties);
|
||||
return new ConcurrentPulsarListenerContainerFactory<>(pulsarConsumerFactory, containerProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PulsarReaderFactory.class)
|
||||
DefaultPulsarReaderFactory<?> pulsarReaderFactory(PulsarClient pulsarClient,
|
||||
ObjectProvider<ReaderBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ReaderBuilderCustomizer<?>> customizers = new ArrayList<>();
|
||||
customizers.add(this.propertiesMapper::customizeReaderBuilder);
|
||||
customizers.addAll(customizersProvider.orderedStream().toList());
|
||||
List<ReaderBuilderCustomizer<Object>> lambdaSafeCustomizers = List
|
||||
.of((builder) -> applyReaderBuilderCustomizers(customizers, builder));
|
||||
return new DefaultPulsarReaderFactory<>(pulsarClient, lambdaSafeCustomizers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyReaderBuilderCustomizers(List<ReaderBuilderCustomizer<?>> customizers, ReaderBuilder<?> builder) {
|
||||
LambdaSafe.callbacks(ReaderBuilderCustomizer.class, customizers, builder)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "pulsarReaderContainerFactory")
|
||||
DefaultPulsarReaderContainerFactory<?> pulsarReaderContainerFactory(PulsarReaderFactory<?> pulsarReaderFactory,
|
||||
SchemaResolver schemaResolver) {
|
||||
PulsarReaderContainerProperties readerContainerProperties = new PulsarReaderContainerProperties();
|
||||
readerContainerProperties.setSchemaResolver(schemaResolver);
|
||||
this.propertiesMapper.customizeReaderContainerProperties(readerContainerProperties);
|
||||
return new DefaultPulsarReaderContainerFactory<>(pulsarReaderFactory, readerContainerProperties);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnablePulsar
|
||||
@ConditionalOnMissingBean(name = { PulsarAnnotationSupportBeanNames.PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME,
|
||||
PulsarAnnotationSupportBeanNames.PULSAR_READER_ANNOTATION_PROCESSOR_BEAN_NAME })
|
||||
static class EnablePulsarConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.admin.PulsarAdminBuilder;
|
||||
import org.apache.pulsar.client.api.ClientBuilder;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.PulsarClientException;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.SchemaInfo;
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.TypeMapping;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.pulsar.core.DefaultPulsarClientFactory;
|
||||
import org.springframework.pulsar.core.DefaultSchemaResolver;
|
||||
import org.springframework.pulsar.core.DefaultTopicResolver;
|
||||
import org.springframework.pulsar.core.PulsarAdminBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarClientBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.PulsarClientFactory;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
import org.springframework.pulsar.function.PulsarFunction;
|
||||
import org.springframework.pulsar.function.PulsarFunctionAdministration;
|
||||
import org.springframework.pulsar.function.PulsarSink;
|
||||
import org.springframework.pulsar.function.PulsarSource;
|
||||
|
||||
/**
|
||||
* Common configuration used by both {@link PulsarAutoConfiguration} and
|
||||
* {@link PulsarReactiveAutoConfiguration}. A separate configuration class is used so that
|
||||
* {@link PulsarAutoConfiguration} can be excluded for reactive only application.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(PulsarProperties.class)
|
||||
class PulsarConfiguration {
|
||||
|
||||
private final PulsarProperties properties;
|
||||
|
||||
private final PulsarPropertiesMapper propertiesMapper;
|
||||
|
||||
PulsarConfiguration(PulsarProperties properties) {
|
||||
this.properties = properties;
|
||||
this.propertiesMapper = new PulsarPropertiesMapper(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PulsarClientFactory.class)
|
||||
DefaultPulsarClientFactory pulsarClientFactory(ObjectProvider<PulsarClientBuilderCustomizer> customizersProvider) {
|
||||
List<PulsarClientBuilderCustomizer> allCustomizers = new ArrayList<>();
|
||||
allCustomizers.add(this.propertiesMapper::customizeClientBuilder);
|
||||
allCustomizers.addAll(customizersProvider.orderedStream().toList());
|
||||
DefaultPulsarClientFactory clientFactory = new DefaultPulsarClientFactory(
|
||||
(clientBuilder) -> applyClientBuilderCustomizers(allCustomizers, clientBuilder));
|
||||
return clientFactory;
|
||||
}
|
||||
|
||||
private void applyClientBuilderCustomizers(List<PulsarClientBuilderCustomizer> customizers,
|
||||
ClientBuilder clientBuilder) {
|
||||
customizers.forEach((customizer) -> customizer.customize(clientBuilder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
PulsarClient pulsarClient(PulsarClientFactory clientFactory) throws PulsarClientException {
|
||||
return clientFactory.createClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
PulsarAdministration pulsarAdministration(
|
||||
ObjectProvider<PulsarAdminBuilderCustomizer> pulsarAdminBuilderCustomizers) {
|
||||
List<PulsarAdminBuilderCustomizer> allCustomizers = new ArrayList<>();
|
||||
allCustomizers.add(this.propertiesMapper::customizeAdminBuilder);
|
||||
allCustomizers.addAll(pulsarAdminBuilderCustomizers.orderedStream().toList());
|
||||
return new PulsarAdministration((adminBuilder) -> applyAdminBuilderCustomizers(allCustomizers, adminBuilder));
|
||||
}
|
||||
|
||||
private void applyAdminBuilderCustomizers(List<PulsarAdminBuilderCustomizer> customizers,
|
||||
PulsarAdminBuilder adminBuilder) {
|
||||
customizers.forEach((customizer) -> customizer.customize(adminBuilder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SchemaResolver.class)
|
||||
DefaultSchemaResolver pulsarSchemaResolver(ObjectProvider<SchemaResolverCustomizer<?>> schemaResolverCustomizers) {
|
||||
DefaultSchemaResolver schemaResolver = new DefaultSchemaResolver();
|
||||
addCustomSchemaMappings(schemaResolver, this.properties.getDefaults().getTypeMappings());
|
||||
applySchemaResolverCustomizers(schemaResolverCustomizers.orderedStream().toList(), schemaResolver);
|
||||
return schemaResolver;
|
||||
}
|
||||
|
||||
private void addCustomSchemaMappings(DefaultSchemaResolver schemaResolver, List<TypeMapping> typeMappings) {
|
||||
if (typeMappings != null) {
|
||||
typeMappings.forEach((typeMapping) -> addCustomSchemaMapping(schemaResolver, typeMapping));
|
||||
}
|
||||
}
|
||||
|
||||
private void addCustomSchemaMapping(DefaultSchemaResolver schemaResolver, TypeMapping typeMapping) {
|
||||
SchemaInfo schemaInfo = typeMapping.schemaInfo();
|
||||
if (schemaInfo != null) {
|
||||
Class<?> messageType = typeMapping.messageType();
|
||||
SchemaType schemaType = schemaInfo.schemaType();
|
||||
Class<?> messageKeyType = schemaInfo.messageKeyType();
|
||||
Schema<?> schema = schemaResolver.resolveSchema(schemaType, messageType, messageKeyType).orElseThrow();
|
||||
schemaResolver.addCustomSchemaMapping(typeMapping.messageType(), schema);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applySchemaResolverCustomizers(List<SchemaResolverCustomizer<?>> customizers,
|
||||
DefaultSchemaResolver schemaResolver) {
|
||||
LambdaSafe.callbacks(SchemaResolverCustomizer.class, customizers, schemaResolver)
|
||||
.invoke((customizer) -> customizer.customize(schemaResolver));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(TopicResolver.class)
|
||||
DefaultTopicResolver pulsarTopicResolver() {
|
||||
DefaultTopicResolver topicResolver = new DefaultTopicResolver();
|
||||
List<TypeMapping> typeMappings = this.properties.getDefaults().getTypeMappings();
|
||||
if (typeMappings != null) {
|
||||
typeMappings.forEach((typeMapping) -> addCustomTopicMapping(topicResolver, typeMapping));
|
||||
}
|
||||
return topicResolver;
|
||||
}
|
||||
|
||||
private void addCustomTopicMapping(DefaultTopicResolver topicResolver, TypeMapping typeMapping) {
|
||||
String topicName = typeMapping.topicName();
|
||||
if (topicName != null) {
|
||||
topicResolver.addCustomTopicMapping(typeMapping.messageType(), topicName);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "spring.pulsar.function.enabled", havingValue = "true", matchIfMissing = true)
|
||||
PulsarFunctionAdministration pulsarFunctionAdministration(PulsarAdministration pulsarAdministration,
|
||||
ObjectProvider<PulsarFunction> pulsarFunctions, ObjectProvider<PulsarSink> pulsarSinks,
|
||||
ObjectProvider<PulsarSource> pulsarSources) {
|
||||
PulsarProperties.Function properties = this.properties.getFunction();
|
||||
return new PulsarFunctionAdministration(pulsarAdministration, pulsarFunctions, pulsarSinks, pulsarSources,
|
||||
properties.isFailFast(), properties.isPropagateFailures(), properties.isPropagateStopFailures());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,890 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pulsar.client.api.CompressionType;
|
||||
import org.apache.pulsar.client.api.HashingScheme;
|
||||
import org.apache.pulsar.client.api.MessageRoutingMode;
|
||||
import org.apache.pulsar.client.api.ProducerAccessMode;
|
||||
import org.apache.pulsar.client.api.RegexSubscriptionMode;
|
||||
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
|
||||
import org.apache.pulsar.client.api.SubscriptionMode;
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Configuration properties Apache Pulsar.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.pulsar")
|
||||
public class PulsarProperties {
|
||||
|
||||
private final Client client = new Client();
|
||||
|
||||
private final Admin admin = new Admin();
|
||||
|
||||
private final Defaults defaults = new Defaults();
|
||||
|
||||
private final Function function = new Function();
|
||||
|
||||
private final Producer producer = new Producer();
|
||||
|
||||
private final Consumer consumer = new Consumer();
|
||||
|
||||
private final Listener listener = new Listener();
|
||||
|
||||
private final Reader reader = new Reader();
|
||||
|
||||
private final Template template = new Template();
|
||||
|
||||
public Client getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public Admin getAdmin() {
|
||||
return this.admin;
|
||||
}
|
||||
|
||||
public Defaults getDefaults() {
|
||||
return this.defaults;
|
||||
}
|
||||
|
||||
public Producer getProducer() {
|
||||
return this.producer;
|
||||
}
|
||||
|
||||
public Consumer getConsumer() {
|
||||
return this.consumer;
|
||||
}
|
||||
|
||||
public Listener getListener() {
|
||||
return this.listener;
|
||||
}
|
||||
|
||||
public Reader getReader() {
|
||||
return this.reader;
|
||||
}
|
||||
|
||||
public Function getFunction() {
|
||||
return this.function;
|
||||
}
|
||||
|
||||
public Template getTemplate() {
|
||||
return this.template;
|
||||
}
|
||||
|
||||
public static class Client {
|
||||
|
||||
/**
|
||||
* Pulsar service URL in the format '(pulsar|pulsar+ssl)://host:port'.
|
||||
*/
|
||||
private String serviceUrl = "pulsar://localhost:6650";
|
||||
|
||||
/**
|
||||
* Client operation timeout.
|
||||
*/
|
||||
private Duration operationTimeout = Duration.ofSeconds(30);
|
||||
|
||||
/**
|
||||
* Client lookup timeout.
|
||||
*/
|
||||
private Duration lookupTimeout = Duration.ofMillis(-1); // FIXME
|
||||
|
||||
/**
|
||||
* Duration to wait for a connection to a broker to be established.
|
||||
*/
|
||||
private Duration connectionTimeout = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* Authentication settings.
|
||||
*/
|
||||
private final Authentication authentication = new Authentication();
|
||||
|
||||
public String getServiceUrl() {
|
||||
return this.serviceUrl;
|
||||
}
|
||||
|
||||
public void setServiceUrl(String serviceUrl) {
|
||||
this.serviceUrl = serviceUrl;
|
||||
}
|
||||
|
||||
public Duration getOperationTimeout() {
|
||||
return this.operationTimeout;
|
||||
}
|
||||
|
||||
public void setOperationTimeout(Duration operationTimeout) {
|
||||
this.operationTimeout = operationTimeout;
|
||||
}
|
||||
|
||||
public Duration getLookupTimeout() {
|
||||
return this.lookupTimeout;
|
||||
}
|
||||
|
||||
public void setLookupTimeout(Duration lookupTimeout) {
|
||||
this.lookupTimeout = lookupTimeout;
|
||||
}
|
||||
|
||||
public Duration getConnectionTimeout() {
|
||||
return this.connectionTimeout;
|
||||
}
|
||||
|
||||
public void setConnectionTimeout(Duration connectionTimeout) {
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Admin {
|
||||
|
||||
/**
|
||||
* Pulsar web URL for the admin endpoint in the format '(http|https)://host:port'.
|
||||
*/
|
||||
private String serviceUrl = "http://localhost:8080";
|
||||
|
||||
/**
|
||||
* Duration to wait for a connection to server to be established.
|
||||
*/
|
||||
private Duration connectionTimeout = Duration.ofMinutes(1);
|
||||
|
||||
/**
|
||||
* Server response read time out for any request.
|
||||
*/
|
||||
private Duration readTimeout = Duration.ofMinutes(1);
|
||||
|
||||
/**
|
||||
* Server request time out for any request.
|
||||
*/
|
||||
private Duration requestTimeout = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* Authentication settings.
|
||||
*/
|
||||
private final Authentication authentication = new Authentication();
|
||||
|
||||
public String getServiceUrl() {
|
||||
return this.serviceUrl;
|
||||
}
|
||||
|
||||
public void setServiceUrl(String serviceUrl) {
|
||||
this.serviceUrl = serviceUrl;
|
||||
}
|
||||
|
||||
public Duration getConnectionTimeout() {
|
||||
return this.connectionTimeout;
|
||||
}
|
||||
|
||||
public void setConnectionTimeout(Duration connectionTimeout) {
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
}
|
||||
|
||||
public Duration getReadTimeout() {
|
||||
return this.readTimeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(Duration readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
public Duration getRequestTimeout() {
|
||||
return this.requestTimeout;
|
||||
}
|
||||
|
||||
public void setRequestTimeout(Duration requestTimeout) {
|
||||
this.requestTimeout = requestTimeout;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Defaults {
|
||||
|
||||
/**
|
||||
* List of mappings from message type to topic name and schema info to use as a
|
||||
* defaults when a topic name and/or schema is not explicitly specified when
|
||||
* producing or consuming messages of the mapped type.
|
||||
*/
|
||||
private List<TypeMapping> typeMappings = new ArrayList<>();
|
||||
|
||||
public List<TypeMapping> getTypeMappings() {
|
||||
return this.typeMappings;
|
||||
}
|
||||
|
||||
public void setTypeMappings(List<TypeMapping> typeMappings) {
|
||||
this.typeMappings = typeMappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping from message type to topic and/or schema info to use (at least one of
|
||||
* {@code topicName} or {@code schemaInfo} must be specified.
|
||||
*
|
||||
* @param messageType the message type
|
||||
* @param topicName the topic name
|
||||
* @param schemaInfo the schema info
|
||||
*/
|
||||
public record TypeMapping(Class<?> messageType, String topicName, SchemaInfo schemaInfo) {
|
||||
|
||||
public TypeMapping {
|
||||
Assert.notNull(messageType, "messageType must not be null");
|
||||
Assert.isTrue(topicName != null || schemaInfo != null,
|
||||
"At least one of topicName or schemaInfo must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a schema - holds enough information to construct an actual schema
|
||||
* instance.
|
||||
*
|
||||
* @param schemaType schema type
|
||||
* @param messageKeyType message key type (required for key value type)
|
||||
*/
|
||||
public record SchemaInfo(SchemaType schemaType, Class<?> messageKeyType) {
|
||||
|
||||
public SchemaInfo {
|
||||
Assert.notNull(schemaType, "schemaType must not be null");
|
||||
Assert.isTrue(schemaType != SchemaType.NONE, "schemaType 'NONE' not supported");
|
||||
Assert.isTrue(messageKeyType == null || schemaType == SchemaType.KEY_VALUE,
|
||||
"messageKeyType can only be set when schemaType is KEY_VALUE");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Function {
|
||||
|
||||
/**
|
||||
* Whether to stop processing further function creates/updates when a failure
|
||||
* occurs.
|
||||
*/
|
||||
private boolean failFast = true;
|
||||
|
||||
/**
|
||||
* Whether to throw an exception if any failure is encountered during server
|
||||
* startup while creating/updating functions.
|
||||
*/
|
||||
private boolean propagateFailures = true;
|
||||
|
||||
/**
|
||||
* Whether to throw an exception if any failure is encountered during server
|
||||
* shutdown while enforcing stop policy on functions.
|
||||
*/
|
||||
private boolean propagateStopFailures = false;
|
||||
|
||||
public boolean isFailFast() {
|
||||
return this.failFast;
|
||||
}
|
||||
|
||||
public void setFailFast(boolean failFast) {
|
||||
this.failFast = failFast;
|
||||
}
|
||||
|
||||
public boolean isPropagateFailures() {
|
||||
return this.propagateFailures;
|
||||
}
|
||||
|
||||
public void setPropagateFailures(boolean propagateFailures) {
|
||||
this.propagateFailures = propagateFailures;
|
||||
}
|
||||
|
||||
public boolean isPropagateStopFailures() {
|
||||
return this.propagateStopFailures;
|
||||
}
|
||||
|
||||
public void setPropagateStopFailures(boolean propagateStopFailures) {
|
||||
this.propagateStopFailures = propagateStopFailures;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Producer {
|
||||
|
||||
/**
|
||||
* Name for the producer. If not assigned, a unique name is generated.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Topic the producer will publish to.
|
||||
*/
|
||||
private String topicName;
|
||||
|
||||
/**
|
||||
* Time before a message has to be acknowledged by the broker.
|
||||
*/
|
||||
private Duration sendTimeout = Duration.ofSeconds(30);
|
||||
|
||||
/**
|
||||
* Message routing mode for a partitioned producer.
|
||||
*/
|
||||
private MessageRoutingMode messageRoutingMode = MessageRoutingMode.RoundRobinPartition;
|
||||
|
||||
/**
|
||||
* Message hashing scheme to choose the partition to which the message is
|
||||
* published.
|
||||
*/
|
||||
private HashingScheme hashingScheme = HashingScheme.JavaStringHash;
|
||||
|
||||
/**
|
||||
* Whether to automatically batch messages.
|
||||
*/
|
||||
private boolean batchingEnabled = true;
|
||||
|
||||
/**
|
||||
* Whether to split large-size messages into multiple chunks.
|
||||
*/
|
||||
private boolean chunkingEnabled;
|
||||
|
||||
/**
|
||||
* Message compression type.
|
||||
*/
|
||||
private CompressionType compressionType;
|
||||
|
||||
/**
|
||||
* Type of access to the topic the producer requires.
|
||||
*/
|
||||
private ProducerAccessMode accessMode = ProducerAccessMode.Shared;
|
||||
|
||||
private final Cache cache = new Cache();
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getTopicName() {
|
||||
return this.topicName;
|
||||
}
|
||||
|
||||
public void setTopicName(String topicName) {
|
||||
this.topicName = topicName;
|
||||
}
|
||||
|
||||
public Duration getSendTimeout() {
|
||||
return this.sendTimeout;
|
||||
}
|
||||
|
||||
public void setSendTimeout(Duration sendTimeout) {
|
||||
this.sendTimeout = sendTimeout;
|
||||
}
|
||||
|
||||
public MessageRoutingMode getMessageRoutingMode() {
|
||||
return this.messageRoutingMode;
|
||||
}
|
||||
|
||||
public void setMessageRoutingMode(MessageRoutingMode messageRoutingMode) {
|
||||
this.messageRoutingMode = messageRoutingMode;
|
||||
}
|
||||
|
||||
public HashingScheme getHashingScheme() {
|
||||
return this.hashingScheme;
|
||||
}
|
||||
|
||||
public void setHashingScheme(HashingScheme hashingScheme) {
|
||||
this.hashingScheme = hashingScheme;
|
||||
}
|
||||
|
||||
public boolean isBatchingEnabled() {
|
||||
return this.batchingEnabled;
|
||||
}
|
||||
|
||||
public void setBatchingEnabled(boolean batchingEnabled) {
|
||||
this.batchingEnabled = batchingEnabled;
|
||||
}
|
||||
|
||||
public boolean isChunkingEnabled() {
|
||||
return this.chunkingEnabled;
|
||||
}
|
||||
|
||||
public void setChunkingEnabled(boolean chunkingEnabled) {
|
||||
this.chunkingEnabled = chunkingEnabled;
|
||||
}
|
||||
|
||||
public CompressionType getCompressionType() {
|
||||
return this.compressionType;
|
||||
}
|
||||
|
||||
public void setCompressionType(CompressionType compressionType) {
|
||||
this.compressionType = compressionType;
|
||||
}
|
||||
|
||||
public ProducerAccessMode getAccessMode() {
|
||||
return this.accessMode;
|
||||
}
|
||||
|
||||
public void setAccessMode(ProducerAccessMode accessMode) {
|
||||
this.accessMode = accessMode;
|
||||
}
|
||||
|
||||
public Cache getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
public static class Cache {
|
||||
|
||||
/**
|
||||
* Time period to expire unused entries in the cache.
|
||||
*/
|
||||
private Duration expireAfterAccess = Duration.ofMinutes(1);
|
||||
|
||||
/**
|
||||
* Maximum size of cache (entries).
|
||||
*/
|
||||
private long maximumSize = 1000L;
|
||||
|
||||
/**
|
||||
* Initial size of cache.
|
||||
*/
|
||||
private int initialCapacity = 50;
|
||||
|
||||
public Duration getExpireAfterAccess() {
|
||||
return this.expireAfterAccess;
|
||||
}
|
||||
|
||||
public void setExpireAfterAccess(Duration expireAfterAccess) {
|
||||
this.expireAfterAccess = expireAfterAccess;
|
||||
}
|
||||
|
||||
public long getMaximumSize() {
|
||||
return this.maximumSize;
|
||||
}
|
||||
|
||||
public void setMaximumSize(long maximumSize) {
|
||||
this.maximumSize = maximumSize;
|
||||
}
|
||||
|
||||
public int getInitialCapacity() {
|
||||
return this.initialCapacity;
|
||||
}
|
||||
|
||||
public void setInitialCapacity(int initialCapacity) {
|
||||
this.initialCapacity = initialCapacity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Consumer {
|
||||
|
||||
/**
|
||||
* Consumer name to identify a particular consumer from the topic stats.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Topics the consumer subscribes to.
|
||||
*/
|
||||
private List<String> topics;
|
||||
|
||||
/**
|
||||
* Pattern for topics the consumer subscribes to.
|
||||
*/
|
||||
private Pattern topicsPattern;
|
||||
|
||||
/**
|
||||
* Priority level for shared subscription consumers.
|
||||
*/
|
||||
private int priorityLevel = 0;
|
||||
|
||||
/**
|
||||
* Whether to read messages from the compacted topic rather than the full message
|
||||
* backlog.
|
||||
*/
|
||||
private boolean readCompacted = false;
|
||||
|
||||
/**
|
||||
* Dead letter policy to use.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private DeadLetterPolicy deadLetterPolicy;
|
||||
|
||||
/**
|
||||
* Consumer subscription properties.
|
||||
*/
|
||||
private final Subscription subscription = new Subscription();
|
||||
|
||||
/**
|
||||
* Whether to auto retry messages.
|
||||
*/
|
||||
private boolean retryEnable = false;
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Consumer.Subscription getSubscription() {
|
||||
return this.subscription;
|
||||
}
|
||||
|
||||
public List<String> getTopics() {
|
||||
return this.topics;
|
||||
}
|
||||
|
||||
public void setTopics(List<String> topics) {
|
||||
this.topics = topics;
|
||||
}
|
||||
|
||||
public Pattern getTopicsPattern() {
|
||||
return this.topicsPattern;
|
||||
}
|
||||
|
||||
public void setTopicsPattern(Pattern topicsPattern) {
|
||||
this.topicsPattern = topicsPattern;
|
||||
}
|
||||
|
||||
public int getPriorityLevel() {
|
||||
return this.priorityLevel;
|
||||
}
|
||||
|
||||
public void setPriorityLevel(int priorityLevel) {
|
||||
this.priorityLevel = priorityLevel;
|
||||
}
|
||||
|
||||
public boolean isReadCompacted() {
|
||||
return this.readCompacted;
|
||||
}
|
||||
|
||||
public void setReadCompacted(boolean readCompacted) {
|
||||
this.readCompacted = readCompacted;
|
||||
}
|
||||
|
||||
public DeadLetterPolicy getDeadLetterPolicy() {
|
||||
return this.deadLetterPolicy;
|
||||
}
|
||||
|
||||
public void setDeadLetterPolicy(DeadLetterPolicy deadLetterPolicy) {
|
||||
this.deadLetterPolicy = deadLetterPolicy;
|
||||
}
|
||||
|
||||
public boolean isRetryEnable() {
|
||||
return this.retryEnable;
|
||||
}
|
||||
|
||||
public void setRetryEnable(boolean retryEnable) {
|
||||
this.retryEnable = retryEnable;
|
||||
}
|
||||
|
||||
public static class Subscription {
|
||||
|
||||
/**
|
||||
* Subscription name for the consumer.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Position where to initialize a newly created subscription.
|
||||
*/
|
||||
private SubscriptionInitialPosition initialPosition = SubscriptionInitialPosition.Latest;
|
||||
|
||||
/**
|
||||
* Subscription mode to be used when subscribing to the topic.
|
||||
*/
|
||||
private SubscriptionMode mode = SubscriptionMode.Durable;
|
||||
|
||||
/**
|
||||
* Determines which type of topics (persistent, non-persistent, or all) the
|
||||
* consumer should be subscribed to when using pattern subscriptions.
|
||||
*/
|
||||
private RegexSubscriptionMode topicsMode = RegexSubscriptionMode.PersistentOnly;
|
||||
|
||||
/**
|
||||
* Subscription type to be used when subscribing to a topic.
|
||||
*/
|
||||
private SubscriptionType type = SubscriptionType.Exclusive;
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public SubscriptionInitialPosition getInitialPosition() {
|
||||
return this.initialPosition;
|
||||
}
|
||||
|
||||
public void setInitialPosition(SubscriptionInitialPosition initialPosition) {
|
||||
this.initialPosition = initialPosition;
|
||||
}
|
||||
|
||||
public SubscriptionMode getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
public void setMode(SubscriptionMode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public RegexSubscriptionMode getTopicsMode() {
|
||||
return this.topicsMode;
|
||||
}
|
||||
|
||||
public void setTopicsMode(RegexSubscriptionMode topicsMode) {
|
||||
this.topicsMode = topicsMode;
|
||||
}
|
||||
|
||||
public SubscriptionType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(SubscriptionType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class DeadLetterPolicy {
|
||||
|
||||
/**
|
||||
* Maximum number of times that a message will be redelivered before being
|
||||
* sent to the dead letter queue.
|
||||
*/
|
||||
private int maxRedeliverCount;
|
||||
|
||||
/**
|
||||
* Name of the retry topic where the failing messages will be sent.
|
||||
*/
|
||||
private String retryLetterTopic;
|
||||
|
||||
/**
|
||||
* Name of the dead topic where the failing messages will be sent.
|
||||
*/
|
||||
private String deadLetterTopic;
|
||||
|
||||
/**
|
||||
* Name of the initial subscription of the dead letter topic. When not set,
|
||||
* the initial subscription will not be created. However, when the property is
|
||||
* set then the broker's 'allowAutoSubscriptionCreation' must be enabled or
|
||||
* the DLQ producer will fail.
|
||||
*/
|
||||
private String initialSubscriptionName;
|
||||
|
||||
public int getMaxRedeliverCount() {
|
||||
return this.maxRedeliverCount;
|
||||
}
|
||||
|
||||
public void setMaxRedeliverCount(int maxRedeliverCount) {
|
||||
this.maxRedeliverCount = maxRedeliverCount;
|
||||
}
|
||||
|
||||
public String getRetryLetterTopic() {
|
||||
return this.retryLetterTopic;
|
||||
}
|
||||
|
||||
public void setRetryLetterTopic(String retryLetterTopic) {
|
||||
this.retryLetterTopic = retryLetterTopic;
|
||||
}
|
||||
|
||||
public String getDeadLetterTopic() {
|
||||
return this.deadLetterTopic;
|
||||
}
|
||||
|
||||
public void setDeadLetterTopic(String deadLetterTopic) {
|
||||
this.deadLetterTopic = deadLetterTopic;
|
||||
}
|
||||
|
||||
public String getInitialSubscriptionName() {
|
||||
return this.initialSubscriptionName;
|
||||
}
|
||||
|
||||
public void setInitialSubscriptionName(String initialSubscriptionName) {
|
||||
this.initialSubscriptionName = initialSubscriptionName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Listener {
|
||||
|
||||
/**
|
||||
* SchemaType of the consumed messages.
|
||||
*/
|
||||
private SchemaType schemaType;
|
||||
|
||||
/**
|
||||
* Whether to record observations for when the Observations API is available and
|
||||
* the client supports it.
|
||||
*/
|
||||
private boolean observationEnabled = true;
|
||||
|
||||
public SchemaType getSchemaType() {
|
||||
return this.schemaType;
|
||||
}
|
||||
|
||||
public void setSchemaType(SchemaType schemaType) {
|
||||
this.schemaType = schemaType;
|
||||
}
|
||||
|
||||
public boolean isObservationEnabled() {
|
||||
return this.observationEnabled;
|
||||
}
|
||||
|
||||
public void setObservationEnabled(boolean observationEnabled) {
|
||||
this.observationEnabled = observationEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Reader {
|
||||
|
||||
/**
|
||||
* Reader name.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Topis the reader subscribes to.
|
||||
*/
|
||||
private List<String> topics;
|
||||
|
||||
/**
|
||||
* Subscription name.
|
||||
*/
|
||||
private String subscriptionName;
|
||||
|
||||
/**
|
||||
* Prefix of subscription role.
|
||||
*/
|
||||
private String subscriptionRolePrefix;
|
||||
|
||||
/**
|
||||
* Whether to read messages from a compacted topic rather than a full message
|
||||
* backlog of a topic.
|
||||
*/
|
||||
private boolean readCompacted;
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<String> getTopics() {
|
||||
return this.topics;
|
||||
}
|
||||
|
||||
public void setTopics(List<String> topics) {
|
||||
this.topics = topics;
|
||||
}
|
||||
|
||||
public String getSubscriptionName() {
|
||||
return this.subscriptionName;
|
||||
}
|
||||
|
||||
public void setSubscriptionName(String subscriptionName) {
|
||||
this.subscriptionName = subscriptionName;
|
||||
}
|
||||
|
||||
public String getSubscriptionRolePrefix() {
|
||||
return this.subscriptionRolePrefix;
|
||||
}
|
||||
|
||||
public void setSubscriptionRolePrefix(String subscriptionRolePrefix) {
|
||||
this.subscriptionRolePrefix = subscriptionRolePrefix;
|
||||
}
|
||||
|
||||
public boolean isReadCompacted() {
|
||||
return this.readCompacted;
|
||||
}
|
||||
|
||||
public void setReadCompacted(boolean readCompacted) {
|
||||
this.readCompacted = readCompacted;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Template {
|
||||
|
||||
/**
|
||||
* Whether to record observations for when the Observations API is available.
|
||||
*/
|
||||
private boolean observationsEnabled = true;
|
||||
|
||||
public boolean isObservationsEnabled() {
|
||||
return this.observationsEnabled;
|
||||
}
|
||||
|
||||
public void setObservationsEnabled(boolean observationsEnabled) {
|
||||
this.observationsEnabled = observationsEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Authentication {
|
||||
|
||||
/**
|
||||
* Fully qualified class name of the authentication plugin.
|
||||
*/
|
||||
private String pluginClassName;
|
||||
|
||||
/**
|
||||
* Authentication parameter(s) as a map of parameter names to parameter values.
|
||||
*/
|
||||
private Map<String, String> param = new LinkedHashMap<>();
|
||||
|
||||
public String getPluginClassName() {
|
||||
return this.pluginClassName;
|
||||
}
|
||||
|
||||
public void setPluginClassName(String pluginClassName) {
|
||||
this.pluginClassName = pluginClassName;
|
||||
}
|
||||
|
||||
public Map<String, String> getParam() {
|
||||
return this.param;
|
||||
}
|
||||
|
||||
public void setParam(Map<String, String> param) {
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.pulsar.client.admin.PulsarAdminBuilder;
|
||||
import org.apache.pulsar.client.api.ClientBuilder;
|
||||
import org.apache.pulsar.client.api.ConsumerBuilder;
|
||||
import org.apache.pulsar.client.api.ProducerBuilder;
|
||||
import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException;
|
||||
import org.apache.pulsar.client.api.ReaderBuilder;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.pulsar.listener.PulsarContainerProperties;
|
||||
import org.springframework.pulsar.reader.PulsarReaderContainerProperties;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Helper class used to map {@link PulsarProperties} to various builder customizers.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class PulsarPropertiesMapper {
|
||||
|
||||
private final PulsarProperties properties;
|
||||
|
||||
PulsarPropertiesMapper(PulsarProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
void customizeClientBuilder(ClientBuilder clientBuilder) {
|
||||
PulsarProperties.Client properties = this.properties.getClient();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getServiceUrl).to(clientBuilder::serviceUrl);
|
||||
map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout));
|
||||
map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout));
|
||||
map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout));
|
||||
customizeAuthentication(clientBuilder::authentication, properties.getAuthentication());
|
||||
}
|
||||
|
||||
void customizeAdminBuilder(PulsarAdminBuilder adminBuilder) {
|
||||
PulsarProperties.Admin properties = this.properties.getAdmin();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getServiceUrl).to(adminBuilder::serviceHttpUrl);
|
||||
map.from(properties::getConnectionTimeout).to(timeoutProperty(adminBuilder::connectionTimeout));
|
||||
map.from(properties::getReadTimeout).to(timeoutProperty(adminBuilder::readTimeout));
|
||||
map.from(properties::getRequestTimeout).to(timeoutProperty(adminBuilder::requestTimeout));
|
||||
customizeAuthentication(adminBuilder::authentication, properties.getAuthentication());
|
||||
}
|
||||
|
||||
private void customizeAuthentication(AuthenticationConsumer authentication,
|
||||
PulsarProperties.Authentication properties) {
|
||||
if (StringUtils.hasText(properties.getPluginClassName())) {
|
||||
try {
|
||||
authentication.accept(properties.getPluginClassName(), properties.getParam());
|
||||
}
|
||||
catch (UnsupportedAuthenticationException ex) {
|
||||
throw new IllegalStateException("Unable to configure Pulsar authentication", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<T> void customizeProducerBuilder(ProducerBuilder<T> producerBuilder) {
|
||||
PulsarProperties.Producer properties = this.properties.getProducer();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(producerBuilder::producerName);
|
||||
map.from(properties::getTopicName).to(producerBuilder::topic);
|
||||
map.from(properties::getSendTimeout).to(timeoutProperty(producerBuilder::sendTimeout));
|
||||
map.from(properties::getMessageRoutingMode).to(producerBuilder::messageRoutingMode);
|
||||
map.from(properties::getHashingScheme).to(producerBuilder::hashingScheme);
|
||||
map.from(properties::isBatchingEnabled).to(producerBuilder::enableBatching);
|
||||
map.from(properties::isChunkingEnabled).to(producerBuilder::enableChunking);
|
||||
map.from(properties::getCompressionType).to(producerBuilder::compressionType);
|
||||
map.from(properties::getAccessMode).to(producerBuilder::accessMode);
|
||||
}
|
||||
|
||||
<T> void customizeConsumerBuilder(ConsumerBuilder<T> consumerBuilder) {
|
||||
PulsarProperties.Consumer properties = this.properties.getConsumer();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(consumerBuilder::consumerName);
|
||||
map.from(properties::getTopics).as(ArrayList::new).to(consumerBuilder::topics);
|
||||
map.from(properties::getTopicsPattern).to(consumerBuilder::topicsPattern);
|
||||
map.from(properties::getPriorityLevel).to(consumerBuilder::priorityLevel);
|
||||
map.from(properties::isReadCompacted).to(consumerBuilder::readCompacted);
|
||||
map.from(properties::getDeadLetterPolicy).as(DeadLetterPolicyMapper::map).to(consumerBuilder::deadLetterPolicy);
|
||||
map.from(properties::isRetryEnable).to(consumerBuilder::enableRetry);
|
||||
customizeConsumerBuilderSubscription(consumerBuilder);
|
||||
}
|
||||
|
||||
private void customizeConsumerBuilderSubscription(ConsumerBuilder<?> consumerBuilder) {
|
||||
PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(consumerBuilder::subscriptionName);
|
||||
map.from(properties::getInitialPosition).to(consumerBuilder::subscriptionInitialPosition);
|
||||
map.from(properties::getMode).to(consumerBuilder::subscriptionMode);
|
||||
map.from(properties::getTopicsMode).to(consumerBuilder::subscriptionTopicsMode);
|
||||
map.from(properties::getType).to(consumerBuilder::subscriptionType);
|
||||
}
|
||||
|
||||
void customizeContainerProperties(PulsarContainerProperties containerProperties) {
|
||||
customizePulsarContainerConsumerSubscriptionProperties(containerProperties);
|
||||
customizePulsarContainerListenerProperties(containerProperties);
|
||||
}
|
||||
|
||||
private void customizePulsarContainerConsumerSubscriptionProperties(PulsarContainerProperties containerProperties) {
|
||||
PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getType).to(containerProperties::setSubscriptionType);
|
||||
}
|
||||
|
||||
private void customizePulsarContainerListenerProperties(PulsarContainerProperties containerProperties) {
|
||||
PulsarProperties.Listener properties = this.properties.getListener();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getSchemaType).to(containerProperties::setSchemaType);
|
||||
map.from(properties::isObservationEnabled).to(containerProperties::setObservationEnabled);
|
||||
}
|
||||
|
||||
<T> void customizeReaderBuilder(ReaderBuilder<T> readerBuilder) {
|
||||
PulsarProperties.Reader properties = this.properties.getReader();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(readerBuilder::readerName);
|
||||
map.from(properties::getTopics).to(readerBuilder::topics);
|
||||
map.from(properties::getSubscriptionName).to(readerBuilder::subscriptionName);
|
||||
map.from(properties::getSubscriptionRolePrefix).to(readerBuilder::subscriptionRolePrefix);
|
||||
map.from(properties::isReadCompacted).to(readerBuilder::readCompacted);
|
||||
}
|
||||
|
||||
void customizeReaderContainerProperties(PulsarReaderContainerProperties readerContainerProperties) {
|
||||
PulsarProperties.Reader properties = this.properties.getReader();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getTopics).to(readerContainerProperties::setTopics);
|
||||
}
|
||||
|
||||
private Consumer<Duration> timeoutProperty(BiConsumer<Integer, TimeUnit> setter) {
|
||||
return (duration) -> setter.accept((int) duration.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private interface AuthenticationConsumer {
|
||||
|
||||
void accept(String authPluginClassName, Map<String, String> authParams)
|
||||
throws UnsupportedAuthenticationException;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory;
|
||||
import org.apache.pulsar.reactive.client.adapter.ProducerCacheProvider;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
import org.apache.pulsar.reactive.client.producercache.CaffeineShadedProducerCacheProvider;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.pulsar.config.PulsarAnnotationSupportBeanNames;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.reactive.config.annotation.EnableReactivePulsar;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageSenderBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring for Apache Pulsar
|
||||
* Reactive.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Christophe Bornet
|
||||
* @since 3.2.0
|
||||
*/
|
||||
@AutoConfiguration(after = PulsarAutoConfiguration.class)
|
||||
@ConditionalOnClass({ PulsarClient.class, ReactivePulsarClient.class, ReactivePulsarTemplate.class })
|
||||
@Import(PulsarConfiguration.class)
|
||||
public class PulsarReactiveAutoConfiguration {
|
||||
|
||||
private final PulsarProperties properties;
|
||||
|
||||
private final PulsarReactivePropertiesMapper propertiesMapper;
|
||||
|
||||
PulsarReactiveAutoConfiguration(PulsarProperties properties) {
|
||||
this.properties = properties;
|
||||
this.propertiesMapper = new PulsarReactivePropertiesMapper(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ReactivePulsarClient reactivePulsarClient(PulsarClient pulsarClient) {
|
||||
return AdaptedReactivePulsarClientFactory.create(pulsarClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ProducerCacheProvider.class)
|
||||
@ConditionalOnClass(CaffeineShadedProducerCacheProvider.class)
|
||||
@ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true)
|
||||
CaffeineShadedProducerCacheProvider reactivePulsarProducerCacheProvider() {
|
||||
PulsarProperties.Producer.Cache properties = this.properties.getProducer().getCache();
|
||||
return new CaffeineShadedProducerCacheProvider(properties.getExpireAfterAccess(), Duration.ofMinutes(10),
|
||||
properties.getMaximumSize(), properties.getInitialCapacity());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true)
|
||||
ReactiveMessageSenderCache reactivePulsarMessageSenderCache(
|
||||
ObjectProvider<ProducerCacheProvider> producerCacheProvider) {
|
||||
return reactivePulsarMessageSenderCache(producerCacheProvider.getIfAvailable());
|
||||
}
|
||||
|
||||
private ReactiveMessageSenderCache reactivePulsarMessageSenderCache(ProducerCacheProvider producerCacheProvider) {
|
||||
return (producerCacheProvider != null) ? AdaptedReactivePulsarClientFactory.createCache(producerCacheProvider)
|
||||
: AdaptedReactivePulsarClientFactory.createCache();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ReactivePulsarSenderFactory.class)
|
||||
DefaultReactivePulsarSenderFactory<?> reactivePulsarSenderFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
ObjectProvider<ReactiveMessageSenderCache> reactiveMessageSenderCache, TopicResolver topicResolver,
|
||||
ObjectProvider<ReactiveMessageSenderBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ReactiveMessageSenderBuilderCustomizer<?>> customizers = new ArrayList<>();
|
||||
customizers.add(this.propertiesMapper::customizeMessageSenderBuilder);
|
||||
customizers.addAll(customizersProvider.orderedStream().toList());
|
||||
List<ReactiveMessageSenderBuilderCustomizer<Object>> lambdaSafeCustomizers = List
|
||||
.of((builder) -> applyMessageSenderBuilderCustomizers(customizers, builder));
|
||||
return DefaultReactivePulsarSenderFactory.builderFor(reactivePulsarClient)
|
||||
.withDefaultConfigCustomizers(lambdaSafeCustomizers)
|
||||
.withMessageSenderCache(reactiveMessageSenderCache.getIfAvailable())
|
||||
.withTopicResolver(topicResolver)
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyMessageSenderBuilderCustomizers(List<ReactiveMessageSenderBuilderCustomizer<?>> customizers,
|
||||
ReactiveMessageSenderBuilder<?> builder) {
|
||||
LambdaSafe.callbacks(ReactiveMessageSenderBuilderCustomizer.class, customizers, builder)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ReactivePulsarConsumerFactory.class)
|
||||
DefaultReactivePulsarConsumerFactory<?> reactivePulsarConsumerFactory(
|
||||
ReactivePulsarClient pulsarReactivePulsarClient,
|
||||
ObjectProvider<ReactiveMessageConsumerBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ReactiveMessageConsumerBuilderCustomizer<?>> customizers = new ArrayList<>();
|
||||
customizers.add(this.propertiesMapper::customizeMessageConsumerBuilder);
|
||||
customizers.addAll(customizersProvider.orderedStream().toList());
|
||||
List<ReactiveMessageConsumerBuilderCustomizer<Object>> lambdaSafeCustomizers = List
|
||||
.of((builder) -> applyMessageConsumerBuilderCustomizers(customizers, builder));
|
||||
return new DefaultReactivePulsarConsumerFactory<>(pulsarReactivePulsarClient, lambdaSafeCustomizers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyMessageConsumerBuilderCustomizers(List<ReactiveMessageConsumerBuilderCustomizer<?>> customizers,
|
||||
ReactiveMessageConsumerBuilder<?> builder) {
|
||||
LambdaSafe.callbacks(ReactiveMessageConsumerBuilderCustomizer.class, customizers, builder)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "reactivePulsarListenerContainerFactory")
|
||||
DefaultReactivePulsarListenerContainerFactory<?> reactivePulsarListenerContainerFactory(
|
||||
ReactivePulsarConsumerFactory<Object> reactivePulsarConsumerFactory, SchemaResolver schemaResolver,
|
||||
TopicResolver topicResolver) {
|
||||
ReactivePulsarContainerProperties<Object> containerProperties = new ReactivePulsarContainerProperties<>();
|
||||
containerProperties.setSchemaResolver(schemaResolver);
|
||||
containerProperties.setTopicResolver(topicResolver);
|
||||
this.propertiesMapper.customizeContainerProperties(containerProperties);
|
||||
return new DefaultReactivePulsarListenerContainerFactory<>(reactivePulsarConsumerFactory, containerProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ReactivePulsarReaderFactory.class)
|
||||
DefaultReactivePulsarReaderFactory<?> reactivePulsarReaderFactory(ReactivePulsarClient reactivePulsarClient,
|
||||
ObjectProvider<ReactiveMessageReaderBuilderCustomizer<?>> customizersProvider) {
|
||||
List<ReactiveMessageReaderBuilderCustomizer<?>> customizers = new ArrayList<>();
|
||||
customizers.add(this.propertiesMapper::customizeMessageReaderBuilder);
|
||||
customizers.addAll(customizersProvider.orderedStream().toList());
|
||||
List<ReactiveMessageReaderBuilderCustomizer<Object>> lambdaSafeCustomizers = List
|
||||
.of((builder) -> applyMessageReaderBuilderCustomizers(customizers, builder));
|
||||
return new DefaultReactivePulsarReaderFactory<>(reactivePulsarClient, lambdaSafeCustomizers);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void applyMessageReaderBuilderCustomizers(List<ReactiveMessageReaderBuilderCustomizer<?>> customizers,
|
||||
ReactiveMessageReaderBuilder<?> builder) {
|
||||
LambdaSafe.callbacks(ReactiveMessageReaderBuilderCustomizer.class, customizers, builder)
|
||||
.invoke((customizer) -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ReactivePulsarTemplate<?> pulsarReactiveTemplate(ReactivePulsarSenderFactory<?> reactivePulsarSenderFactory,
|
||||
SchemaResolver schemaResolver, TopicResolver topicResolver) {
|
||||
return new ReactivePulsarTemplate<>(reactivePulsarSenderFactory, schemaResolver, topicResolver);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableReactivePulsar
|
||||
@ConditionalOnMissingBean(
|
||||
name = PulsarAnnotationSupportBeanNames.REACTIVE_PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
|
||||
static class EnableReactivePulsarConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
|
||||
|
||||
/**
|
||||
* Helper class used to map reactive {@link PulsarProperties} to various builder
|
||||
* customizers.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class PulsarReactivePropertiesMapper {
|
||||
|
||||
private final PulsarProperties properties;
|
||||
|
||||
PulsarReactivePropertiesMapper(PulsarProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
<T> void customizeMessageSenderBuilder(ReactiveMessageSenderBuilder<T> builder) {
|
||||
PulsarProperties.Producer properties = this.properties.getProducer();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(builder::producerName);
|
||||
map.from(properties::getTopicName).to(builder::topic);
|
||||
map.from(properties::getSendTimeout).to(builder::sendTimeout);
|
||||
map.from(properties::getMessageRoutingMode).to(builder::messageRoutingMode);
|
||||
map.from(properties::getHashingScheme).to(builder::hashingScheme);
|
||||
map.from(properties::isBatchingEnabled).to(builder::batchingEnabled);
|
||||
map.from(properties::isChunkingEnabled).to(builder::chunkingEnabled);
|
||||
map.from(properties::getCompressionType).to(builder::compressionType);
|
||||
map.from(properties::getAccessMode).to(builder::accessMode);
|
||||
}
|
||||
|
||||
<T> void customizeMessageConsumerBuilder(ReactiveMessageConsumerBuilder<T> builder) {
|
||||
PulsarProperties.Consumer properties = this.properties.getConsumer();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(builder::consumerName);
|
||||
map.from(properties::getTopics).as(ArrayList::new).to(builder::topics);
|
||||
map.from(properties::getTopicsPattern).to(builder::topicsPattern);
|
||||
map.from(properties::getPriorityLevel).to(builder::priorityLevel);
|
||||
map.from(properties::isReadCompacted).to(builder::readCompacted);
|
||||
map.from(properties::getDeadLetterPolicy).as(DeadLetterPolicyMapper::map).to(builder::deadLetterPolicy);
|
||||
map.from(properties::isRetryEnable).to(builder::retryLetterTopicEnable);
|
||||
customizerMessageConsumerBuilderSubscription(builder);
|
||||
}
|
||||
|
||||
private <T> void customizerMessageConsumerBuilderSubscription(ReactiveMessageConsumerBuilder<T> builder) {
|
||||
PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(builder::subscriptionName);
|
||||
map.from(properties::getInitialPosition).to(builder::subscriptionInitialPosition);
|
||||
map.from(properties::getMode).to(builder::subscriptionMode);
|
||||
map.from(properties::getTopicsMode).to(builder::topicsPatternSubscriptionMode);
|
||||
map.from(properties::getType).to(builder::subscriptionType);
|
||||
}
|
||||
|
||||
<T> void customizeContainerProperties(ReactivePulsarContainerProperties<T> containerProperties) {
|
||||
customizePulsarContainerConsumerSubscriptionProperties(containerProperties);
|
||||
customizePulsarContainerListenerProperties(containerProperties);
|
||||
}
|
||||
|
||||
private void customizePulsarContainerConsumerSubscriptionProperties(
|
||||
ReactivePulsarContainerProperties<?> containerProperties) {
|
||||
PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getType).to(containerProperties::setSubscriptionType);
|
||||
}
|
||||
|
||||
private void customizePulsarContainerListenerProperties(ReactivePulsarContainerProperties<?> containerProperties) {
|
||||
PulsarProperties.Listener properties = this.properties.getListener();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getSchemaType).to(containerProperties::setSchemaType);
|
||||
}
|
||||
|
||||
void customizeMessageReaderBuilder(ReactiveMessageReaderBuilder<?> builder) {
|
||||
PulsarProperties.Reader properties = this.properties.getReader();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getName).to(builder::readerName);
|
||||
map.from(properties::getTopics).to(builder::topics);
|
||||
map.from(properties::getSubscriptionName).to(builder::subscriptionName);
|
||||
map.from(properties::getSubscriptionRolePrefix).to(builder::generatedSubscriptionNamePrefix);
|
||||
map.from(properties::isReadCompacted).to(builder::readCompacted);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for Spring for Apache Pulsar.
|
||||
*/
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.assertj.core.api.AssertDelegateTarget;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Test utility used to check customizers are called correctly.
|
||||
*
|
||||
* @param <C> the customizer type
|
||||
* @param <T> the target class that is customized
|
||||
* @author Phillip Webb
|
||||
* @author Chris Bono
|
||||
*/
|
||||
final class Customizers<C, T> {
|
||||
|
||||
private final BiConsumer<C, T> customizeAction;
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Customizers(Class<?> targetClass, BiConsumer<C, T> customizeAction) {
|
||||
this.customizeAction = customizeAction;
|
||||
this.targetClass = (Class<T>) targetClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance by getting the value from a field.
|
||||
* @param source the source to extract the customizers from
|
||||
* @param fieldName the field name
|
||||
* @return a new {@link CustomizersAssert} instance
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
CustomizersAssert fromField(Object source, String fieldName) {
|
||||
return new CustomizersAssert(ReflectionTestUtils.getField(source, fieldName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Customizers} instance.
|
||||
* @param <C> the customizer class
|
||||
* @param <T> the target class that is customized
|
||||
* @param targetClass the target class that is customized
|
||||
* @param customizeAction the customizer action to take
|
||||
* @return a new {@link Customizers} instance
|
||||
*/
|
||||
static <C, T> Customizers<C, T> of(Class<?> targetClass, BiConsumer<C, T> customizeAction) {
|
||||
return new Customizers<>(targetClass, customizeAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assertions that can be applied to customizers.
|
||||
*/
|
||||
final class CustomizersAssert implements AssertDelegateTarget {
|
||||
|
||||
private final List<C> customizers;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private CustomizersAssert(Object customizers) {
|
||||
this.customizers = (customizers instanceof List) ? (List<C>) customizers : List.of((C) customizers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the customize method is called in a specified order. It is expected
|
||||
* that each customizer has set a unique value so the expected values can be used
|
||||
* as a verify step.
|
||||
* @param <V> the value type
|
||||
* @param call the call the customizer makes
|
||||
* @param expectedValues the expected values
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
<V> void callsInOrder(BiConsumer<T, V> call, V... expectedValues) {
|
||||
T target = mock(Customizers.this.targetClass);
|
||||
BiConsumer<C, T> customizeAction = Customizers.this.customizeAction;
|
||||
this.customizers.forEach((customizer) -> customizeAction.accept(customizer, target));
|
||||
InOrder ordered = inOrder(target);
|
||||
for (V expectedValue : expectedValues) {
|
||||
call.accept(ordered.verify(target), expectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link DeadLetterPolicyMapper}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DeadLetterPolicyMapperTests {
|
||||
|
||||
@Test
|
||||
void map() {
|
||||
PulsarProperties.Consumer.DeadLetterPolicy properties = new PulsarProperties.Consumer.DeadLetterPolicy();
|
||||
properties.setMaxRedeliverCount(100);
|
||||
properties.setRetryLetterTopic("my-retry-topic");
|
||||
properties.setDeadLetterTopic("my-dlt-topic");
|
||||
properties.setInitialSubscriptionName("my-initial-subscription");
|
||||
DeadLetterPolicy policy = DeadLetterPolicyMapper.map(properties);
|
||||
assertThat(policy.getMaxRedeliverCount()).isEqualTo(100);
|
||||
assertThat(policy.getRetryLetterTopic()).isEqualTo("my-retry-topic");
|
||||
assertThat(policy.getDeadLetterTopic()).isEqualTo("my-dlt-topic");
|
||||
assertThat(policy.getInitialSubscriptionName()).isEqualTo("my-initial-subscription");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapWhenMaxRedeliverCountIsNotPositiveThrowsException() {
|
||||
PulsarProperties.Consumer.DeadLetterPolicy properties = new PulsarProperties.Consumer.DeadLetterPolicy();
|
||||
properties.setMaxRedeliverCount(0);
|
||||
assertThatIllegalStateException().isThrownBy(() -> DeadLetterPolicyMapper.map(properties))
|
||||
.withMessage("Pulsar DeadLetterPolicy must have a positive 'max-redelivery-count' property value");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClientException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PulsarContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link PulsarAutoConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class PulsarAutoConfigurationIntegrationTests {
|
||||
|
||||
@Container
|
||||
private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar())
|
||||
.withStartupAttempts(2)
|
||||
.withStartupTimeout(Duration.ofMinutes(3));
|
||||
|
||||
private static final CountDownLatch LISTEN_LATCH = new CountDownLatch(1);
|
||||
|
||||
private static final String TOPIC = "pacit-hello-topic";
|
||||
|
||||
@DynamicPropertySource
|
||||
static void pulsarProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl);
|
||||
registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void appStartsWithAutoConfiguredSpringPulsarComponents(
|
||||
@Autowired(required = false) PulsarTemplate<String> pulsarTemplate) {
|
||||
assertThat(pulsarTemplate).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateCanBeAccessedDuringWebRequest(@Autowired TestRestTemplate restTemplate) throws InterruptedException {
|
||||
assertThat(restTemplate.getForObject("/hello", String.class)).startsWith("Hello World -> ");
|
||||
assertThat(LISTEN_LATCH.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportAutoConfiguration({ DispatcherServletAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class,
|
||||
PulsarAutoConfiguration.class, PulsarReactiveAutoConfiguration.class })
|
||||
@Import(TestWebController.class)
|
||||
static class TestConfiguration {
|
||||
|
||||
@PulsarListener(subscriptionName = TOPIC + "-sub", topics = TOPIC)
|
||||
void listen(String ignored) {
|
||||
LISTEN_LATCH.countDown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestWebController {
|
||||
|
||||
private final PulsarTemplate<String> pulsarTemplate;
|
||||
|
||||
TestWebController(PulsarTemplate<String> pulsarTemplate) {
|
||||
this.pulsarTemplate = pulsarTemplate;
|
||||
}
|
||||
|
||||
@GetMapping("/hello")
|
||||
String sayHello() throws PulsarClientException {
|
||||
return "Hello World -> " + this.pulsarTemplate.send(TOPIC, "hello");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,519 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.apache.pulsar.client.api.ConsumerBuilder;
|
||||
import org.apache.pulsar.client.api.ProducerBuilder;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.ReaderBuilder;
|
||||
import org.apache.pulsar.client.api.interceptor.ProducerInterceptor;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.pulsar.annotation.PulsarBootstrapConfiguration;
|
||||
import org.springframework.pulsar.annotation.PulsarListenerAnnotationBeanPostProcessor;
|
||||
import org.springframework.pulsar.annotation.PulsarReaderAnnotationBeanPostProcessor;
|
||||
import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory;
|
||||
import org.springframework.pulsar.config.PulsarListenerContainerFactory;
|
||||
import org.springframework.pulsar.config.PulsarListenerEndpointRegistry;
|
||||
import org.springframework.pulsar.config.PulsarReaderEndpointRegistry;
|
||||
import org.springframework.pulsar.core.CachingPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.ConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.DefaultPulsarClientFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarReaderFactory;
|
||||
import org.springframework.pulsar.core.DefaultSchemaResolver;
|
||||
import org.springframework.pulsar.core.DefaultTopicResolver;
|
||||
import org.springframework.pulsar.core.ProducerBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.PulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarReaderFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.ReaderBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarAutoConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Alexander Preuß
|
||||
* @author Soby Chacko
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PulsarAutoConfigurationTests {
|
||||
|
||||
private static final String INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR = "org.springframework.pulsar.config.internalPulsarListenerAnnotationProcessor";
|
||||
|
||||
private static final String INTERNAL_PULSAR_READER_ANNOTATION_PROCESSOR = "org.springframework.pulsar.config.internalPulsarReaderAnnotationProcessor";
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(PulsarAutoConfiguration.class))
|
||||
.withBean(PulsarClient.class, () -> mock(PulsarClient.class));
|
||||
|
||||
@Test
|
||||
void whenPulsarNotOnClasspathAutoConfigurationIsSkipped() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarAutoConfiguration.class))
|
||||
.withClassLoader(new FilteredClassLoader(PulsarClient.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenSpringPulsarNotOnClasspathAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(PulsarTemplate.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomPulsarListenerAnnotationProcessorDefinedAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withBean(INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR, String.class, () -> "bean")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarBootstrapConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomPulsarReaderAnnotationProcessorDefinedAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withBean(INTERNAL_PULSAR_READER_ANNOTATION_PROCESSOR, String.class, () -> "bean")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarBootstrapConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoConfiguresBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PulsarConfiguration.class)
|
||||
.hasSingleBean(DefaultPulsarClientFactory.class)
|
||||
.hasSingleBean(PulsarClient.class)
|
||||
.hasSingleBean(PulsarAdministration.class)
|
||||
.hasSingleBean(DefaultSchemaResolver.class)
|
||||
.hasSingleBean(DefaultTopicResolver.class)
|
||||
.hasSingleBean(CachingPulsarProducerFactory.class)
|
||||
.hasSingleBean(PulsarTemplate.class)
|
||||
.hasSingleBean(DefaultPulsarConsumerFactory.class)
|
||||
.hasSingleBean(ConcurrentPulsarListenerContainerFactory.class)
|
||||
.hasSingleBean(DefaultPulsarReaderFactory.class)
|
||||
.hasSingleBean(DefaultPulsarReaderContainerFactory.class)
|
||||
.hasSingleBean(PulsarListenerAnnotationBeanPostProcessor.class)
|
||||
.hasSingleBean(PulsarListenerEndpointRegistry.class)
|
||||
.hasSingleBean(PulsarReaderAnnotationBeanPostProcessor.class)
|
||||
.hasSingleBean(PulsarReaderEndpointRegistry.class));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ProducerFactoryTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
PulsarProducerFactory<String> producerFactory = mock(PulsarProducerFactory.class);
|
||||
this.contextRunner
|
||||
.withBean("customPulsarProducerFactory", PulsarProducerFactory.class, () -> producerFactory)
|
||||
.run((context) -> assertThat(context).getBean(PulsarProducerFactory.class).isSameAs(producerFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoPropertiesUsesCachingPulsarProducerFactory() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(PulsarProducerFactory.class)
|
||||
.isExactlyInstanceOf(CachingPulsarProducerFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingDisabledUsesDefaultPulsarProducerFactory() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=false")
|
||||
.run((context) -> assertThat(context).getBean(PulsarProducerFactory.class)
|
||||
.isExactlyInstanceOf(DefaultPulsarProducerFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingEnabledUsesCachingPulsarProducerFactory() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
|
||||
.run((context) -> assertThat(context).getBean(PulsarProducerFactory.class)
|
||||
.isExactlyInstanceOf(CachingPulsarProducerFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingEnabledAndCaffeineNotOnClasspathStillUsesCaffeine() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(Caffeine.class))
|
||||
.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
|
||||
.run((context) -> {
|
||||
assertThat(context).getBean(CachingPulsarProducerFactory.class)
|
||||
.extracting("producerCache")
|
||||
.extracting(Object::getClass)
|
||||
.extracting(Class::getName)
|
||||
.isEqualTo("org.springframework.pulsar.core.CaffeineCacheProvider");
|
||||
assertThat(context).getBean(CachingPulsarProducerFactory.class)
|
||||
.extracting("producerCache.cache")
|
||||
.extracting(Object::getClass)
|
||||
.extracting(Class::getName)
|
||||
.asString()
|
||||
.startsWith("org.springframework.pulsar.shade.com.github.benmanes.caffeine.cache.");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomCachingPropertiesCreatesConfiguredBean() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.pulsar.producer.cache.expire-after-access=100s",
|
||||
"spring.pulsar.producer.cache.maximum-size=5150",
|
||||
"spring.pulsar.producer.cache.initial-capacity=200")
|
||||
.run((context) -> assertThat(context).getBean(CachingPulsarProducerFactory.class)
|
||||
.extracting("producerCache.cache.cache")
|
||||
.hasFieldOrPropertyWithValue("maximum", 5150L)
|
||||
.hasFieldOrPropertyWithValue("expiresAfterAccessNanos", TimeUnit.SECONDS.toNanos(100)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasTopicNamePropertyCreatesConfiguredBean() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.topic-name=my-topic")
|
||||
.run((context) -> assertThat(context).getBean(DefaultPulsarProducerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("defaultTopic", "my-topic"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.pulsar.producer.topic-name=my-topic",
|
||||
"spring.pulsar.producer.cache.enabled=false")
|
||||
.run((context) -> assertThat(context).getBean(DefaultPulsarProducerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class))
|
||||
.hasFieldOrPropertyWithValue("topicResolver", context.getBean(TopicResolver.class)));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = { true, false })
|
||||
<T> void whenHasUserDefinedCustomizersAppliesInCorrectOrder(boolean cachingEnabled) {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.pulsar.producer.cache.enabled=" + cachingEnabled,
|
||||
"spring.pulsar.producer.name=fromPropsCustomizer")
|
||||
.withUserConfiguration(ProducerBuilderCustomizersConfig.class)
|
||||
.run((context) -> {
|
||||
DefaultPulsarProducerFactory<?> producerFactory = context
|
||||
.getBean(DefaultPulsarProducerFactory.class);
|
||||
Customizers<ProducerBuilderCustomizer<T>, ProducerBuilder<T>> customizers = Customizers
|
||||
.of(ProducerBuilder.class, ProducerBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(producerFactory, "defaultConfigCustomizers")).callsInOrder(
|
||||
ProducerBuilder::producerName, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ProducerBuilderCustomizersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ProducerBuilderCustomizer<?> customizerFoo() {
|
||||
return (builder) -> builder.producerName("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ProducerBuilderCustomizer<?> customizerBar() {
|
||||
return (builder) -> builder.producerName("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TemplateTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
PulsarTemplate<String> template = mock(PulsarTemplate.class);
|
||||
this.contextRunner.withBean("customPulsarTemplate", PulsarTemplate.class, () -> template)
|
||||
.run((context) -> assertThat(context).getBean(PulsarTemplate.class).isSameAs(template));
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
PulsarProducerFactory<?> producerFactory = mock(PulsarProducerFactory.class);
|
||||
SchemaResolver schemaResolver = mock(SchemaResolver.class);
|
||||
TopicResolver topicResolver = mock(TopicResolver.class);
|
||||
this.contextRunner
|
||||
.withBean("customPulsarProducerFactory", PulsarProducerFactory.class, () -> producerFactory)
|
||||
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver)
|
||||
.withBean("topicResolver", TopicResolver.class, () -> topicResolver)
|
||||
.run((context) -> assertThat(context).getBean(PulsarTemplate.class)
|
||||
.hasFieldOrPropertyWithValue("producerFactory", producerFactory)
|
||||
.hasFieldOrPropertyWithValue("schemaResolver", schemaResolver)
|
||||
.hasFieldOrPropertyWithValue("topicResolver", topicResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUseDefinedProducerInterceptorInjectsBean() {
|
||||
ProducerInterceptor interceptor = mock(ProducerInterceptor.class);
|
||||
this.contextRunner.withBean("customProducerInterceptor", ProducerInterceptor.class, () -> interceptor)
|
||||
.run((context) -> assertThat(context).getBean(PulsarTemplate.class)
|
||||
.extracting("interceptors")
|
||||
.asList()
|
||||
.contains(interceptor));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUseDefinedProducerInterceptorsInjectsBeansInCorrectOrder() {
|
||||
this.contextRunner.withUserConfiguration(InterceptorTestConfiguration.class)
|
||||
.run((context) -> assertThat(context).getBean(PulsarTemplate.class)
|
||||
.extracting("interceptors")
|
||||
.asList()
|
||||
.containsExactly(context.getBean("interceptorBar"), context.getBean("interceptorFoo")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoPropertiesEnablesObservation() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(PulsarTemplate.class)
|
||||
.hasFieldOrPropertyWithValue("observationEnabled", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationsEnabledEnablesObservation() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.template.observations-enabled=true")
|
||||
.run((context) -> assertThat(context).getBean(PulsarTemplate.class)
|
||||
.hasFieldOrPropertyWithValue("observationEnabled", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationsDisabledDoesNotEnableObservation() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.template.observations-enabled=false")
|
||||
.run((context) -> assertThat(context).getBean(PulsarTemplate.class)
|
||||
.hasFieldOrPropertyWithValue("observationEnabled", false));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class InterceptorTestConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ProducerInterceptor interceptorFoo() {
|
||||
return mock(ProducerInterceptor.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ProducerInterceptor interceptorBar() {
|
||||
return mock(ProducerInterceptor.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConsumerFactoryTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
PulsarConsumerFactory<String> consumerFactory = mock(PulsarConsumerFactory.class);
|
||||
this.contextRunner
|
||||
.withBean("customPulsarConsumerFactory", PulsarConsumerFactory.class, () -> consumerFactory)
|
||||
.run((context) -> assertThat(context).getBean(PulsarConsumerFactory.class).isSameAs(consumerFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(DefaultPulsarConsumerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
<T> void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.consumer.name=fromPropsCustomizer")
|
||||
.withUserConfiguration(ConsumerBuilderCustomizersConfig.class)
|
||||
.run((context) -> {
|
||||
DefaultPulsarConsumerFactory<?> consumerFactory = context
|
||||
.getBean(DefaultPulsarConsumerFactory.class);
|
||||
Customizers<ConsumerBuilderCustomizer<T>, ConsumerBuilder<T>> customizers = Customizers
|
||||
.of(ConsumerBuilder.class, ConsumerBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(consumerFactory, "defaultConfigCustomizers")).callsInOrder(
|
||||
ConsumerBuilder::consumerName, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ConsumerBuilderCustomizersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ConsumerBuilderCustomizer<?> customizerFoo() {
|
||||
return (builder) -> builder.consumerName("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ConsumerBuilderCustomizer<?> customizerBar() {
|
||||
return (builder) -> builder.consumerName("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ListenerTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedListenerContainerFactoryBeanDoesNotAutoConfigureBean() {
|
||||
PulsarListenerContainerFactory listenerContainerFactory = mock(PulsarListenerContainerFactory.class);
|
||||
this.contextRunner
|
||||
.withBean("pulsarListenerContainerFactory", PulsarListenerContainerFactory.class,
|
||||
() -> listenerContainerFactory)
|
||||
.run((context) -> assertThat(context).getBean(PulsarListenerContainerFactory.class)
|
||||
.isSameAs(listenerContainerFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void injectsExpectedBeans() {
|
||||
PulsarConsumerFactory<?> consumerFactory = mock(PulsarConsumerFactory.class);
|
||||
SchemaResolver schemaResolver = mock(SchemaResolver.class);
|
||||
TopicResolver topicResolver = mock(TopicResolver.class);
|
||||
this.contextRunner.withBean("pulsarConsumerFactory", PulsarConsumerFactory.class, () -> consumerFactory)
|
||||
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver)
|
||||
.withBean("topicResolver", TopicResolver.class, () -> topicResolver)
|
||||
.run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("consumerFactory", consumerFactory)
|
||||
.extracting(ConcurrentPulsarListenerContainerFactory::getContainerProperties)
|
||||
.hasFieldOrPropertyWithValue("schemaResolver", schemaResolver)
|
||||
.hasFieldOrPropertyWithValue("topicResolver", topicResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void whenHasUserDefinedListenerAnnotationBeanPostProcessorBeanDoesNotAutoConfigureBean() {
|
||||
PulsarListenerAnnotationBeanPostProcessor<String> listenerAnnotationBeanPostProcessor = mock(
|
||||
PulsarListenerAnnotationBeanPostProcessor.class);
|
||||
this.contextRunner
|
||||
.withBean("org.springframework.pulsar.config.internalPulsarListenerAnnotationProcessor",
|
||||
PulsarListenerAnnotationBeanPostProcessor.class, () -> listenerAnnotationBeanPostProcessor)
|
||||
.run((context) -> assertThat(context).getBean(PulsarListenerAnnotationBeanPostProcessor.class)
|
||||
.isSameAs(listenerAnnotationBeanPostProcessor));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomProperties() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.listener.schema-type=avro");
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new)).run((context) -> {
|
||||
ConcurrentPulsarListenerContainerFactory<?> factory = context
|
||||
.getBean(ConcurrentPulsarListenerContainerFactory.class);
|
||||
assertThat(factory.getContainerProperties().getSchemaType()).isEqualTo(SchemaType.AVRO);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoPropertiesEnablesObservation() {
|
||||
this.contextRunner
|
||||
.run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationsEnabledEnablesObservation() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.listener.observation-enabled=true")
|
||||
.run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenObservationsDisabledDoesNotEnableObservation() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.listener.observation-enabled=false")
|
||||
.run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class)
|
||||
.hasFieldOrPropertyWithValue("containerProperties.observationEnabled", false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReaderFactoryTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
PulsarReaderFactory<String> readerFactory = mock(PulsarReaderFactory.class);
|
||||
this.contextRunner.withBean("customPulsarReaderFactory", PulsarReaderFactory.class, () -> readerFactory)
|
||||
.run((context) -> assertThat(context).getBean(PulsarReaderFactory.class).isSameAs(readerFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(DefaultPulsarReaderFactory.class)
|
||||
.hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
<T> void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.reader.name=fromPropsCustomizer")
|
||||
.withUserConfiguration(ReaderBuilderCustomizersConfig.class)
|
||||
.run((context) -> {
|
||||
DefaultPulsarReaderFactory<?> readerFactory = context.getBean(DefaultPulsarReaderFactory.class);
|
||||
Customizers<ReaderBuilderCustomizer<T>, ReaderBuilder<T>> customizers = Customizers
|
||||
.of(ReaderBuilder.class, ReaderBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(readerFactory, "defaultConfigCustomizers")).callsInOrder(
|
||||
ReaderBuilder::readerName, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ReaderBuilderCustomizersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ReaderBuilderCustomizer<?> customizerFoo() {
|
||||
return (builder) -> builder.readerName("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ReaderBuilderCustomizer<?> customizerBar() {
|
||||
return (builder) -> builder.readerName("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.pulsar.client.admin.PulsarAdminBuilder;
|
||||
import org.apache.pulsar.client.api.ClientBuilder;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.common.schema.KeyValueEncodingType;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.assertj.core.api.InstanceOfAssertFactory;
|
||||
import org.assertj.core.api.MapAssert;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.pulsar.core.DefaultPulsarClientFactory;
|
||||
import org.springframework.pulsar.core.DefaultSchemaResolver;
|
||||
import org.springframework.pulsar.core.DefaultTopicResolver;
|
||||
import org.springframework.pulsar.core.PulsarAdminBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.PulsarClientBuilderCustomizer;
|
||||
import org.springframework.pulsar.core.PulsarClientFactory;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
import org.springframework.pulsar.function.PulsarFunctionAdministration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Alexander Preuß
|
||||
* @author Soby Chacko
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PulsarConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(PulsarConfiguration.class))
|
||||
.withBean(PulsarClient.class, () -> mock(PulsarClient.class));
|
||||
|
||||
@Nested
|
||||
class ClientTests {
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedClientFactoryBeanDoesNotAutoConfigureBean() {
|
||||
PulsarClientFactory customFactory = mock(PulsarClientFactory.class);
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarConfiguration.class))
|
||||
.withBean("customPulsarClientFactory", PulsarClientFactory.class, () -> customFactory)
|
||||
.run((context) -> assertThat(context).getBean(PulsarClientFactory.class).isSameAs(customFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedClientBeanDoesNotAutoConfigureBean() {
|
||||
PulsarClient customClient = mock(PulsarClient.class);
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarConfiguration.class))
|
||||
.withBean("customPulsarClient", PulsarClient.class, () -> customClient)
|
||||
.run((context) -> assertThat(context).getBean(PulsarClient.class).isSameAs(customClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
PulsarConfigurationTests.this.contextRunner
|
||||
.withUserConfiguration(PulsarClientBuilderCustomizersConfig.class)
|
||||
.withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer")
|
||||
.run((context) -> {
|
||||
DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class);
|
||||
Customizers<PulsarClientBuilderCustomizer, ClientBuilder> customizers = Customizers
|
||||
.of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(clientFactory, "customizer")).callsInOrder(
|
||||
ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class PulsarClientBuilderCustomizersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
PulsarClientBuilderCustomizer customizerFoo() {
|
||||
return (builder) -> builder.serviceUrl("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
PulsarClientBuilderCustomizer customizerBar() {
|
||||
return (builder) -> builder.serviceUrl("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AdministrationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class);
|
||||
this.contextRunner
|
||||
.withBean("customPulsarAdministration", PulsarAdministration.class, () -> pulsarAdministration)
|
||||
.run((context) -> assertThat(context).getBean(PulsarAdministration.class)
|
||||
.isSameAs(pulsarAdministration));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
this.contextRunner.withUserConfiguration(PulsarAdminBuilderCustomizersConfig.class)
|
||||
.withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer")
|
||||
.run((context) -> {
|
||||
PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class);
|
||||
Customizers<PulsarAdminBuilderCustomizer, PulsarAdminBuilder> customizers = Customizers
|
||||
.of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")).callsInOrder(
|
||||
PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer", "fromCustomizer1",
|
||||
"fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class PulsarAdminBuilderCustomizersConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
PulsarAdminBuilderCustomizer customizerFoo() {
|
||||
return (builder) -> builder.serviceHttpUrl("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
PulsarAdminBuilderCustomizer customizerBar() {
|
||||
return (builder) -> builder.serviceHttpUrl("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class SchemaResolverTests {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static final InstanceOfAssertFactory<Map, MapAssert<Class, Schema>> CLASS_SCHEMA_MAP = InstanceOfAssertFactories
|
||||
.map(Class.class, Schema.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
SchemaResolver schemaResolver = mock(SchemaResolver.class);
|
||||
this.contextRunner.withBean("customSchemaResolver", SchemaResolver.class, () -> schemaResolver)
|
||||
.run((context) -> assertThat(context).getBean(SchemaResolver.class).isSameAs(schemaResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedSchemaResolverCustomizer() {
|
||||
SchemaResolverCustomizer<DefaultSchemaResolver> customizer = (schemaResolver) -> schemaResolver
|
||||
.addCustomSchemaMapping(TestRecord.class, Schema.STRING);
|
||||
this.contextRunner.withBean("schemaResolverCustomizer", SchemaResolverCustomizer.class, () -> customizer)
|
||||
.run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class)
|
||||
.extracting(DefaultSchemaResolver::getCustomSchemaMappings, InstanceOfAssertFactories.MAP)
|
||||
.containsEntry(TestRecord.class, Schema.STRING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasDefaultsTypeMappingForPrimitiveAddsToSchemaResolver() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME);
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=STRING");
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new))
|
||||
.run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class)
|
||||
.extracting(DefaultSchemaResolver::getCustomSchemaMappings, InstanceOfAssertFactories.MAP)
|
||||
.containsOnly(entry(TestRecord.class, Schema.STRING)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasDefaultsTypeMappingForStructAddsToSchemaResolver() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME);
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=JSON");
|
||||
Schema<?> expectedSchema = Schema.JSON(TestRecord.class);
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new))
|
||||
.run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class)
|
||||
.extracting(DefaultSchemaResolver::getCustomSchemaMappings, CLASS_SCHEMA_MAP)
|
||||
.hasEntrySatisfying(TestRecord.class, schemaEqualTo(expectedSchema)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasDefaultsTypeMappingForKeyValueAddsToSchemaResolver() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME);
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=key-value");
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type=java.lang.String");
|
||||
Schema<?> expectedSchema = Schema.KeyValue(Schema.STRING, Schema.JSON(TestRecord.class),
|
||||
KeyValueEncodingType.INLINE);
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new))
|
||||
.run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class)
|
||||
.extracting(DefaultSchemaResolver::getCustomSchemaMappings, CLASS_SCHEMA_MAP)
|
||||
.hasEntrySatisfying(TestRecord.class, schemaEqualTo(expectedSchema)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private Consumer<Schema> schemaEqualTo(Schema<?> expected) {
|
||||
return (actual) -> assertThat(actual.getSchemaInfo()).isEqualTo(expected.getSchemaInfo());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TopicResolverTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
TopicResolver topicResolver = mock(TopicResolver.class);
|
||||
this.contextRunner.withBean("customTopicResolver", TopicResolver.class, () -> topicResolver)
|
||||
.run((context) -> assertThat(context).getBean(TopicResolver.class).isSameAs(topicResolver));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasDefaultsTypeMappingAddsToSchemaResolver() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME);
|
||||
properties.add("spring.pulsar.defaults.type-mappings[0].topic-name=foo-topic");
|
||||
properties.add("spring.pulsar.defaults.type-mappings[1].message-type=java.lang.String");
|
||||
properties.add("spring.pulsar.defaults.type-mappings[1].topic-name=string-topic");
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new))
|
||||
.run((context) -> assertThat(context).getBean(TopicResolver.class)
|
||||
.asInstanceOf(InstanceOfAssertFactories.type(DefaultTopicResolver.class))
|
||||
.extracting(DefaultTopicResolver::getCustomTopicMappings, InstanceOfAssertFactories.MAP)
|
||||
.containsOnly(entry(TestRecord.class, "foo-topic"), entry(String.class, "string-topic")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FunctionAdministrationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenNoPropertiesAddsFunctionAdministrationBean() {
|
||||
this.contextRunner.run((context) -> assertThat(context).getBean(PulsarFunctionAdministration.class)
|
||||
.hasFieldOrPropertyWithValue("failFast", Boolean.TRUE)
|
||||
.hasFieldOrPropertyWithValue("propagateFailures", Boolean.TRUE)
|
||||
.hasFieldOrPropertyWithValue("propagateStopFailures", Boolean.FALSE)
|
||||
.hasNoNullFieldsOrProperties() // ensures object providers set
|
||||
.extracting("pulsarAdministration")
|
||||
.isSameAs(context.getBean(PulsarAdministration.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasFunctionPropertiesAppliesPropertiesToBean() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.function.fail-fast=false");
|
||||
properties.add("spring.pulsar.function.propagate-failures=false");
|
||||
properties.add("spring.pulsar.function.propagate-stop-failures=true");
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new))
|
||||
.run((context) -> assertThat(context).getBean(PulsarFunctionAdministration.class)
|
||||
.hasFieldOrPropertyWithValue("failFast", Boolean.FALSE)
|
||||
.hasFieldOrPropertyWithValue("propagateFailures", Boolean.FALSE)
|
||||
.hasFieldOrPropertyWithValue("propagateStopFailures", Boolean.TRUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasFunctionDisabledPropertyDoesNotCreateBean() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.function.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarFunctionAdministration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomFunctionAdministrationBean() {
|
||||
PulsarFunctionAdministration functionAdministration = mock(PulsarFunctionAdministration.class);
|
||||
this.contextRunner.withBean(PulsarFunctionAdministration.class, () -> functionAdministration)
|
||||
.run((context) -> assertThat(context).getBean(PulsarFunctionAdministration.class)
|
||||
.isSameAs(functionAdministration));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record TestRecord() {
|
||||
|
||||
private static final String CLASS_NAME = TestRecord.class.getName();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pulsar.client.admin.PulsarAdminBuilder;
|
||||
import org.apache.pulsar.client.api.ClientBuilder;
|
||||
import org.apache.pulsar.client.api.CompressionType;
|
||||
import org.apache.pulsar.client.api.ConsumerBuilder;
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
import org.apache.pulsar.client.api.HashingScheme;
|
||||
import org.apache.pulsar.client.api.MessageRoutingMode;
|
||||
import org.apache.pulsar.client.api.ProducerAccessMode;
|
||||
import org.apache.pulsar.client.api.ProducerBuilder;
|
||||
import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException;
|
||||
import org.apache.pulsar.client.api.ReaderBuilder;
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer;
|
||||
import org.springframework.pulsar.listener.PulsarContainerProperties;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarPropertiesMapper}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PulsarPropertiesMapperTests {
|
||||
|
||||
@Test
|
||||
void customizeClientBuilderWhenHasNoAuthentication() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getClient().setServiceUrl("https://example.com");
|
||||
properties.getClient().setConnectionTimeout(Duration.ofSeconds(1));
|
||||
properties.getClient().setOperationTimeout(Duration.ofSeconds(2));
|
||||
properties.getClient().setLookupTimeout(Duration.ofSeconds(3));
|
||||
ClientBuilder builder = mock(ClientBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeClientBuilder(builder);
|
||||
then(builder).should().serviceUrl("https://example.com");
|
||||
then(builder).should().connectionTimeout(1000, TimeUnit.MILLISECONDS);
|
||||
then(builder).should().operationTimeout(2000, TimeUnit.MILLISECONDS);
|
||||
then(builder).should().lookupTimeout(3000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeClientBuilderWhenHasAuthentication() throws UnsupportedAuthenticationException {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
Map<String, String> params = Map.of("param", "name");
|
||||
properties.getClient().getAuthentication().setPluginClassName("myclass");
|
||||
properties.getClient().getAuthentication().setParam(params);
|
||||
ClientBuilder builder = mock(ClientBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeClientBuilder(builder);
|
||||
then(builder).should().authentication("myclass", params);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeAdminBuilderWhenHasNoAuthentication() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getAdmin().setServiceUrl("https://example.com");
|
||||
properties.getAdmin().setConnectionTimeout(Duration.ofSeconds(1));
|
||||
properties.getAdmin().setReadTimeout(Duration.ofSeconds(2));
|
||||
properties.getAdmin().setRequestTimeout(Duration.ofSeconds(3));
|
||||
PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder);
|
||||
then(builder).should().serviceHttpUrl("https://example.com");
|
||||
then(builder).should().connectionTimeout(1000, TimeUnit.MILLISECONDS);
|
||||
then(builder).should().readTimeout(2000, TimeUnit.MILLISECONDS);
|
||||
then(builder).should().requestTimeout(3000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeAdminBuilderWhenHasAuthentication() throws UnsupportedAuthenticationException {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
Map<String, String> params = Map.of("param", "name");
|
||||
properties.getAdmin().getAuthentication().setPluginClassName("myclass");
|
||||
properties.getAdmin().getAuthentication().setParam(params);
|
||||
PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder);
|
||||
then(builder).should().authentication("myclass", params);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customizeProducerBuilder() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getProducer().setName("name");
|
||||
properties.getProducer().setTopicName("topicname");
|
||||
properties.getProducer().setSendTimeout(Duration.ofSeconds(1));
|
||||
properties.getProducer().setMessageRoutingMode(MessageRoutingMode.RoundRobinPartition);
|
||||
properties.getProducer().setHashingScheme(HashingScheme.JavaStringHash);
|
||||
properties.getProducer().setBatchingEnabled(false);
|
||||
properties.getProducer().setChunkingEnabled(true);
|
||||
properties.getProducer().setCompressionType(CompressionType.SNAPPY);
|
||||
properties.getProducer().setAccessMode(ProducerAccessMode.Exclusive);
|
||||
ProducerBuilder<Object> builder = mock(ProducerBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeProducerBuilder(builder);
|
||||
then(builder).should().producerName("name");
|
||||
then(builder).should().topic("topicname");
|
||||
then(builder).should().sendTimeout(1000, TimeUnit.MILLISECONDS);
|
||||
then(builder).should().messageRoutingMode(MessageRoutingMode.RoundRobinPartition);
|
||||
then(builder).should().hashingScheme(HashingScheme.JavaStringHash);
|
||||
then(builder).should().enableBatching(false);
|
||||
then(builder).should().enableChunking(true);
|
||||
then(builder).should().compressionType(CompressionType.SNAPPY);
|
||||
then(builder).should().accessMode(ProducerAccessMode.Exclusive);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customizeConsumerBuilder() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
List<String> topics = List.of("mytopic");
|
||||
Pattern topisPattern = Pattern.compile("my-pattern");
|
||||
properties.getConsumer().setName("name");
|
||||
properties.getConsumer().setTopics(topics);
|
||||
properties.getConsumer().setTopicsPattern(topisPattern);
|
||||
properties.getConsumer().setPriorityLevel(123);
|
||||
properties.getConsumer().setReadCompacted(true);
|
||||
Consumer.DeadLetterPolicy deadLetterPolicy = new Consumer.DeadLetterPolicy();
|
||||
deadLetterPolicy.setDeadLetterTopic("my-dlt");
|
||||
deadLetterPolicy.setMaxRedeliverCount(1);
|
||||
properties.getConsumer().setDeadLetterPolicy(deadLetterPolicy);
|
||||
ConsumerBuilder<Object> builder = mock(ConsumerBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeConsumerBuilder(builder);
|
||||
then(builder).should().consumerName("name");
|
||||
then(builder).should().topics(topics);
|
||||
then(builder).should().topicsPattern(topisPattern);
|
||||
then(builder).should().priorityLevel(123);
|
||||
then(builder).should().readCompacted(true);
|
||||
then(builder).should().deadLetterPolicy(new DeadLetterPolicy(1, null, "my-dlt", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeContainerProperties() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getConsumer().getSubscription().setType(SubscriptionType.Shared);
|
||||
properties.getListener().setSchemaType(SchemaType.AVRO);
|
||||
properties.getListener().setObservationEnabled(false);
|
||||
PulsarContainerProperties containerProperties = new PulsarContainerProperties("my-topic-pattern");
|
||||
new PulsarPropertiesMapper(properties).customizeContainerProperties(containerProperties);
|
||||
assertThat(containerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared);
|
||||
assertThat(containerProperties.getSchemaType()).isEqualTo(SchemaType.AVRO);
|
||||
assertThat(containerProperties.isObservationEnabled()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customizeReaderBuilder() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
List<String> topics = List.of("mytopic");
|
||||
properties.getReader().setName("name");
|
||||
properties.getReader().setTopics(topics);
|
||||
properties.getReader().setSubscriptionName("subname");
|
||||
properties.getReader().setSubscriptionRolePrefix("subroleprefix");
|
||||
properties.getReader().setReadCompacted(true);
|
||||
ReaderBuilder<Object> builder = mock(ReaderBuilder.class);
|
||||
new PulsarPropertiesMapper(properties).customizeReaderBuilder(builder);
|
||||
then(builder).should().readerName("name");
|
||||
then(builder).should().topics(topics);
|
||||
then(builder).should().subscriptionName("subname");
|
||||
then(builder).should().subscriptionRolePrefix("subroleprefix");
|
||||
then(builder).should().readCompacted(true);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pulsar.client.api.CompressionType;
|
||||
import org.apache.pulsar.client.api.HashingScheme;
|
||||
import org.apache.pulsar.client.api.MessageRoutingMode;
|
||||
import org.apache.pulsar.client.api.ProducerAccessMode;
|
||||
import org.apache.pulsar.client.api.RegexSubscriptionMode;
|
||||
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
|
||||
import org.apache.pulsar.client.api.SubscriptionMode;
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.SchemaInfo;
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.TypeMapping;
|
||||
import org.springframework.boot.context.properties.bind.BindException;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarProperties}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Christophe Bornet
|
||||
* @author Soby Chacko
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PulsarPropertiesTests {
|
||||
|
||||
private PulsarProperties bindPropeties(Map<String, String> map) {
|
||||
return new Binder(new MapConfigurationPropertySource(map)).bind("spring.pulsar", PulsarProperties.class).get();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ClientProperties {
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.client.service-url", "my-service-url");
|
||||
map.put("spring.pulsar.client.operation-timeout", "1s");
|
||||
map.put("spring.pulsar.client.lookup-timeout", "2s");
|
||||
map.put("spring.pulsar.client.connection-timeout", "12s");
|
||||
PulsarProperties.Client properties = bindPropeties(map).getClient();
|
||||
assertThat(properties.getServiceUrl()).isEqualTo("my-service-url");
|
||||
assertThat(properties.getOperationTimeout()).isEqualTo(Duration.ofMillis(1000));
|
||||
assertThat(properties.getLookupTimeout()).isEqualTo(Duration.ofMillis(2000));
|
||||
assertThat(properties.getConnectionTimeout()).isEqualTo(Duration.ofMillis(12000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindAuthentication() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.client.authentication.plugin-class-name", "com.example.MyAuth");
|
||||
map.put("spring.pulsar.client.authentication.param.token", "1234");
|
||||
PulsarProperties.Client properties = bindPropeties(map).getClient();
|
||||
assertThat(properties.getAuthentication().getPluginClassName()).isEqualTo("com.example.MyAuth");
|
||||
assertThat(properties.getAuthentication().getParam()).containsEntry("token", "1234");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AdminProperties {
|
||||
|
||||
private final String authPluginClassName = "org.apache.pulsar.client.impl.auth.AuthenticationToken";
|
||||
|
||||
private final String authToken = "1234";
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.admin.service-url", "my-service-url");
|
||||
map.put("spring.pulsar.admin.connection-timeout", "12s");
|
||||
map.put("spring.pulsar.admin.read-timeout", "13s");
|
||||
map.put("spring.pulsar.admin.request-timeout", "14s");
|
||||
PulsarProperties.Admin properties = bindPropeties(map).getAdmin();
|
||||
assertThat(properties.getServiceUrl()).isEqualTo("my-service-url");
|
||||
assertThat(properties.getConnectionTimeout()).isEqualTo(Duration.ofSeconds(12));
|
||||
assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(13));
|
||||
assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(14));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindAuthentication() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.admin.authentication.plugin-class-name", this.authPluginClassName);
|
||||
map.put("spring.pulsar.admin.authentication.param.token", this.authToken);
|
||||
PulsarProperties.Admin properties = bindPropeties(map).getAdmin();
|
||||
assertThat(properties.getAuthentication().getPluginClassName()).isEqualTo(this.authPluginClassName);
|
||||
assertThat(properties.getAuthentication().getParam()).containsEntry("token", this.authToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DefaultsProperties {
|
||||
|
||||
@Test
|
||||
void bindWhenNoTypeMappings() {
|
||||
assertThat(new PulsarProperties().getDefaults().getTypeMappings()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenTypeMappingsWithTopicsOnly() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].topic-name", "foo-topic");
|
||||
map.put("spring.pulsar.defaults.type-mappings[1].message-type", String.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[1].topic-name", "string-topic");
|
||||
PulsarProperties.Defaults properties = bindPropeties(map).getDefaults();
|
||||
TypeMapping expectedTopic1 = new TypeMapping(TestMessage.class, "foo-topic", null);
|
||||
TypeMapping expectedTopic2 = new TypeMapping(String.class, "string-topic", null);
|
||||
assertThat(properties.getTypeMappings()).containsExactly(expectedTopic1, expectedTopic2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenTypeMappingsWithSchemaOnly() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON");
|
||||
PulsarProperties.Defaults properties = bindPropeties(map).getDefaults();
|
||||
TypeMapping expected = new TypeMapping(TestMessage.class, null, new SchemaInfo(SchemaType.JSON, null));
|
||||
assertThat(properties.getTypeMappings()).containsExactly(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenTypeMappingsWithTopicAndSchema() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].topic-name", "foo-topic");
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON");
|
||||
PulsarProperties.Defaults properties = bindPropeties(map).getDefaults();
|
||||
TypeMapping expected = new TypeMapping(TestMessage.class, "foo-topic",
|
||||
new SchemaInfo(SchemaType.JSON, null));
|
||||
assertThat(properties.getTypeMappings()).containsExactly(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenTypeMappingsWithKeyValueSchema() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "KEY_VALUE");
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName());
|
||||
PulsarProperties.Defaults properties = bindPropeties(map).getDefaults();
|
||||
TypeMapping expected = new TypeMapping(TestMessage.class, null,
|
||||
new SchemaInfo(SchemaType.KEY_VALUE, String.class));
|
||||
assertThat(properties.getTypeMappings()).containsExactly(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenNoSchemaThrowsException() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName());
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindPropeties(map))
|
||||
.havingRootCause()
|
||||
.withMessageContaining("schemaType must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenSchemaTypeNoneThrowsException() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "NONE");
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindPropeties(map))
|
||||
.havingRootCause()
|
||||
.withMessageContaining("schemaType 'NONE' not supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindWhenMessageKeyTypeSetOnNonKeyValueSchemaThrowsException() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName());
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON");
|
||||
map.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName());
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindPropeties(map))
|
||||
.havingRootCause()
|
||||
.withMessageContaining("messageKeyType can only be set when schemaType is KEY_VALUE");
|
||||
}
|
||||
|
||||
record TestMessage(String value) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class FunctionProperties {
|
||||
|
||||
@Test
|
||||
void defaults() {
|
||||
PulsarProperties.Function properties = new PulsarProperties.Function();
|
||||
assertThat(properties.isFailFast()).isTrue();
|
||||
assertThat(properties.isPropagateFailures()).isTrue();
|
||||
assertThat(properties.isPropagateStopFailures()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> props = new HashMap<>();
|
||||
props.put("spring.pulsar.function.fail-fast", "false");
|
||||
props.put("spring.pulsar.function.propagate-failures", "false");
|
||||
props.put("spring.pulsar.function.propagate-stop-failures", "true");
|
||||
PulsarProperties.Function properties = bindPropeties(props).getFunction();
|
||||
assertThat(properties.isFailFast()).isFalse();
|
||||
assertThat(properties.isPropagateFailures()).isFalse();
|
||||
assertThat(properties.isPropagateStopFailures()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ProducerProperties {
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.producer.name", "my-producer");
|
||||
map.put("spring.pulsar.producer.topic-name", "my-topic");
|
||||
map.put("spring.pulsar.producer.send-timeout", "2s");
|
||||
map.put("spring.pulsar.producer.message-routing-mode", "custompartition");
|
||||
map.put("spring.pulsar.producer.hashing-scheme", "murmur3_32hash");
|
||||
map.put("spring.pulsar.producer.batching-enabled", "false");
|
||||
map.put("spring.pulsar.producer.chunking-enabled", "true");
|
||||
map.put("spring.pulsar.producer.compression-type", "lz4");
|
||||
map.put("spring.pulsar.producer.access-mode", "exclusive");
|
||||
map.put("spring.pulsar.producer.cache.expire-after-access", "2s");
|
||||
map.put("spring.pulsar.producer.cache.maximum-size", "3");
|
||||
map.put("spring.pulsar.producer.cache.initial-capacity", "5");
|
||||
PulsarProperties.Producer properties = bindPropeties(map).getProducer();
|
||||
assertThat(properties.getName()).isEqualTo("my-producer");
|
||||
assertThat(properties.getTopicName()).isEqualTo("my-topic");
|
||||
assertThat(properties.getSendTimeout()).isEqualTo(Duration.ofSeconds(2));
|
||||
assertThat(properties.getMessageRoutingMode()).isEqualTo(MessageRoutingMode.CustomPartition);
|
||||
assertThat(properties.getHashingScheme()).isEqualTo(HashingScheme.Murmur3_32Hash);
|
||||
assertThat(properties.isBatchingEnabled()).isFalse();
|
||||
assertThat(properties.isChunkingEnabled()).isTrue();
|
||||
assertThat(properties.getCompressionType()).isEqualTo(CompressionType.LZ4);
|
||||
assertThat(properties.getAccessMode()).isEqualTo(ProducerAccessMode.Exclusive);
|
||||
assertThat(properties.getCache().getExpireAfterAccess()).isEqualTo(Duration.ofSeconds(2));
|
||||
assertThat(properties.getCache().getMaximumSize()).isEqualTo(3);
|
||||
assertThat(properties.getCache().getInitialCapacity()).isEqualTo(5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConsumerPropertiesTests {
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.consumer.name", "my-consumer");
|
||||
map.put("spring.pulsar.consumer.subscription.initial-position", "earliest");
|
||||
map.put("spring.pulsar.consumer.subscription.mode", "nondurable");
|
||||
map.put("spring.pulsar.consumer.subscription.name", "my-subscription");
|
||||
map.put("spring.pulsar.consumer.subscription.topics-mode", "all-topics");
|
||||
map.put("spring.pulsar.consumer.subscription.type", "shared");
|
||||
map.put("spring.pulsar.consumer.topics[0]", "my-topic");
|
||||
map.put("spring.pulsar.consumer.topics-pattern", "my-pattern");
|
||||
map.put("spring.pulsar.consumer.priority-level", "8");
|
||||
map.put("spring.pulsar.consumer.read-compacted", "true");
|
||||
map.put("spring.pulsar.consumer.dead-letter-policy.max-redeliver-count", "4");
|
||||
map.put("spring.pulsar.consumer.dead-letter-policy.retry-letter-topic", "my-retry-topic");
|
||||
map.put("spring.pulsar.consumer.dead-letter-policy.dead-letter-topic", "my-dlt-topic");
|
||||
map.put("spring.pulsar.consumer.dead-letter-policy.initial-subscription-name", "my-initial-subscription");
|
||||
map.put("spring.pulsar.consumer.retry-enable", "true");
|
||||
PulsarProperties.Consumer properties = bindPropeties(map).getConsumer();
|
||||
assertThat(properties.getName()).isEqualTo("my-consumer");
|
||||
assertThat(properties.getSubscription()).satisfies((subscription) -> {
|
||||
assertThat(subscription.getName()).isEqualTo("my-subscription");
|
||||
assertThat(subscription.getType()).isEqualTo(SubscriptionType.Shared);
|
||||
assertThat(subscription.getMode()).isEqualTo(SubscriptionMode.NonDurable);
|
||||
assertThat(subscription.getInitialPosition()).isEqualTo(SubscriptionInitialPosition.Earliest);
|
||||
assertThat(subscription.getTopicsMode()).isEqualTo(RegexSubscriptionMode.AllTopics);
|
||||
});
|
||||
assertThat(properties.getTopics()).containsExactly("my-topic");
|
||||
assertThat(properties.getTopicsPattern().toString()).isEqualTo("my-pattern");
|
||||
assertThat(properties.getPriorityLevel()).isEqualTo(8);
|
||||
assertThat(properties.isReadCompacted()).isTrue();
|
||||
assertThat(properties.getDeadLetterPolicy()).satisfies((policy) -> {
|
||||
assertThat(policy.getMaxRedeliverCount()).isEqualTo(4);
|
||||
assertThat(policy.getRetryLetterTopic()).isEqualTo("my-retry-topic");
|
||||
assertThat(policy.getDeadLetterTopic()).isEqualTo("my-dlt-topic");
|
||||
assertThat(policy.getInitialSubscriptionName()).isEqualTo("my-initial-subscription");
|
||||
});
|
||||
assertThat(properties.isRetryEnable()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ListenerProperties {
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.listener.schema-type", "avro");
|
||||
map.put("spring.pulsar.listener.observation-enabled", "false");
|
||||
PulsarProperties.Listener properties = bindPropeties(map).getListener();
|
||||
assertThat(properties.getSchemaType()).isEqualTo(SchemaType.AVRO);
|
||||
assertThat(properties.isObservationEnabled()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReaderProperties {
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.reader.name", "my-reader");
|
||||
map.put("spring.pulsar.reader.topics", "my-topic");
|
||||
map.put("spring.pulsar.reader.subscription-name", "my-subscription");
|
||||
map.put("spring.pulsar.reader.subscription-role-prefix", "sub-role");
|
||||
map.put("spring.pulsar.reader.read-compacted", "true");
|
||||
PulsarProperties.Reader properties = bindPropeties(map).getReader();
|
||||
assertThat(properties.getName()).isEqualTo("my-reader");
|
||||
assertThat(properties.getTopics()).containsExactly("my-topic");
|
||||
assertThat(properties.getSubscriptionName()).isEqualTo("my-subscription");
|
||||
assertThat(properties.getSubscriptionRolePrefix()).isEqualTo("sub-role");
|
||||
assertThat(properties.isReadCompacted()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TemplateProperties {
|
||||
|
||||
@Test
|
||||
void bind() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("spring.pulsar.template.observations-enabled", "false");
|
||||
PulsarProperties.Template properties = bindPropeties(map).getTemplate();
|
||||
assertThat(properties.isObservationsEnabled()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,473 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
import org.apache.pulsar.reactive.client.adapter.ProducerCacheProvider;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache;
|
||||
import org.apache.pulsar.reactive.client.api.ReactivePulsarClient;
|
||||
import org.apache.pulsar.reactive.client.producercache.CaffeineShadedProducerCacheProvider;
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.pulsar.core.DefaultSchemaResolver;
|
||||
import org.springframework.pulsar.core.DefaultTopicResolver;
|
||||
import org.springframework.pulsar.core.PulsarAdministration;
|
||||
import org.springframework.pulsar.core.SchemaResolver;
|
||||
import org.springframework.pulsar.core.TopicResolver;
|
||||
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.ReactivePulsarBootstrapConfiguration;
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListenerAnnotationBeanPostProcessor;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory;
|
||||
import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageSenderBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarReactiveAutoConfiguration}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Christophe Bornet
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PulsarReactiveAutoConfigurationTests {
|
||||
|
||||
private static final String INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR = "org.springframework.pulsar.config.internalReactivePulsarListenerAnnotationProcessor";
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(PulsarReactiveAutoConfiguration.class))
|
||||
.withBean(PulsarClient.class, () -> mock(PulsarClient.class));
|
||||
|
||||
@Test
|
||||
void whenPulsarNotOnClasspathAutoConfigurationIsSkipped() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarReactiveAutoConfiguration.class))
|
||||
.withClassLoader(new FilteredClassLoader(PulsarClient.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarReactiveAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactivePulsarNotOnClasspathAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(ReactivePulsarClient.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarReactiveAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenReactiveSpringPulsarNotOnClasspathAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(ReactivePulsarTemplate.class))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(PulsarReactiveAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomPulsarListenerAnnotationProcessorDefinedAutoConfigurationIsSkipped() {
|
||||
this.contextRunner.withBean(INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR, String.class, () -> "bean")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ReactivePulsarBootstrapConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoConfiguresBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PulsarConfiguration.class)
|
||||
.hasSingleBean(PulsarClient.class)
|
||||
.hasSingleBean(PulsarAdministration.class)
|
||||
.hasSingleBean(DefaultSchemaResolver.class)
|
||||
.hasSingleBean(DefaultTopicResolver.class)
|
||||
.hasSingleBean(ReactivePulsarClient.class)
|
||||
.hasSingleBean(CaffeineShadedProducerCacheProvider.class)
|
||||
.hasSingleBean(ReactiveMessageSenderCache.class)
|
||||
.hasSingleBean(DefaultReactivePulsarSenderFactory.class)
|
||||
.hasSingleBean(ReactivePulsarTemplate.class)
|
||||
.hasSingleBean(DefaultReactivePulsarConsumerFactory.class)
|
||||
.hasSingleBean(DefaultReactivePulsarListenerContainerFactory.class)
|
||||
.hasSingleBean(ReactivePulsarListenerAnnotationBeanPostProcessor.class)
|
||||
.hasSingleBean(ReactivePulsarListenerEndpointRegistry.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void injectsExpectedBeansIntoReactivePulsarClient() {
|
||||
this.contextRunner.run((context) -> {
|
||||
PulsarClient pulsarClient = context.getBean(PulsarClient.class);
|
||||
assertThat(context).hasNotFailed()
|
||||
.getBean(ReactivePulsarClient.class)
|
||||
.extracting("reactivePulsarResourceAdapter")
|
||||
.extracting("pulsarClientSupplier", InstanceOfAssertFactories.type(Supplier.class))
|
||||
.extracting(Supplier::get)
|
||||
.isSameAs(pulsarClient);
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = { ReactivePulsarClient.class, ProducerCacheProvider.class, ReactiveMessageSenderCache.class,
|
||||
ReactivePulsarSenderFactory.class, ReactivePulsarConsumerFactory.class, ReactivePulsarReaderFactory.class,
|
||||
ReactivePulsarTemplate.class })
|
||||
<T> void whenHasUserDefinedBeanDoesNotAutoConfigureBean(Class<T> beanClass) {
|
||||
T bean = mock(beanClass);
|
||||
this.contextRunner.withBean(beanClass.getName(), beanClass, () -> bean)
|
||||
.run((context) -> assertThat(context).getBean(beanClass).isSameAs(bean));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class SenderFactoryTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
ReactivePulsarClient client = mock(ReactivePulsarClient.class);
|
||||
ReactiveMessageSenderCache cache = mock(ReactiveMessageSenderCache.class);
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.topic-name=test-topic")
|
||||
.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client)
|
||||
.withBean("customReactiveMessageSenderCache", ReactiveMessageSenderCache.class, () -> cache)
|
||||
.run((context) -> {
|
||||
DefaultReactivePulsarSenderFactory<?> senderFactory = context
|
||||
.getBean(DefaultReactivePulsarSenderFactory.class);
|
||||
assertThat(senderFactory)
|
||||
.extracting("reactivePulsarClient", InstanceOfAssertFactories.type(ReactivePulsarClient.class))
|
||||
.isSameAs(client);
|
||||
assertThat(senderFactory)
|
||||
.extracting("reactiveMessageSenderCache",
|
||||
InstanceOfAssertFactories.type(ReactiveMessageSenderCache.class))
|
||||
.isSameAs(cache);
|
||||
assertThat(senderFactory)
|
||||
.extracting("topicResolver", InstanceOfAssertFactories.type(TopicResolver.class))
|
||||
.isSameAs(context.getBean(TopicResolver.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeansIntoReactiveMessageSenderCache() {
|
||||
ProducerCacheProvider provider = mock(ProducerCacheProvider.class);
|
||||
this.contextRunner.withBean("customProducerCacheProvider", ProducerCacheProvider.class, () -> provider)
|
||||
.run((context) -> assertThat(context).getBean(ReactiveMessageSenderCache.class)
|
||||
.extracting("cacheProvider", InstanceOfAssertFactories.type(ProducerCacheProvider.class))
|
||||
.isSameAs(provider));
|
||||
}
|
||||
|
||||
@Test
|
||||
<T> void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.name=fromPropsCustomizer")
|
||||
.withUserConfiguration(ReactiveMessageSenderBuilderCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
DefaultReactivePulsarSenderFactory<?> producerFactory = context
|
||||
.getBean(DefaultReactivePulsarSenderFactory.class);
|
||||
Customizers<ReactiveMessageSenderBuilderCustomizer<T>, ReactiveMessageSenderBuilder<T>> customizers = Customizers
|
||||
.of(ReactiveMessageSenderBuilder.class, ReactiveMessageSenderBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(producerFactory, "defaultConfigCustomizers")).callsInOrder(
|
||||
ReactiveMessageSenderBuilder::producerName, "fromPropsCustomizer", "fromCustomizer1",
|
||||
"fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ReactiveMessageSenderBuilderCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ReactiveMessageSenderBuilderCustomizer<?> customizerFoo() {
|
||||
return (builder) -> builder.producerName("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ReactiveMessageSenderBuilderCustomizer<?> customizerBar() {
|
||||
return (builder) -> builder.producerName("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TemplateTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void injectsExpectedBeans() {
|
||||
ReactivePulsarSenderFactory senderFactory = mock(ReactivePulsarSenderFactory.class);
|
||||
SchemaResolver schemaResolver = mock(SchemaResolver.class);
|
||||
this.contextRunner
|
||||
.withBean("customReactivePulsarSenderFactory", ReactivePulsarSenderFactory.class, () -> senderFactory)
|
||||
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver)
|
||||
.run((context) -> assertThat(context).getBean(ReactivePulsarTemplate.class).satisfies((template) -> {
|
||||
assertThat(template).extracting("reactiveMessageSenderFactory").isSameAs(senderFactory);
|
||||
assertThat(template).extracting("schemaResolver").isSameAs(schemaResolver);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConsumerFactoryTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
ReactivePulsarClient client = mock(ReactivePulsarClient.class);
|
||||
this.contextRunner.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client)
|
||||
.run((context) -> {
|
||||
ReactivePulsarConsumerFactory<?> consumerFactory = context
|
||||
.getBean(DefaultReactivePulsarConsumerFactory.class);
|
||||
assertThat(consumerFactory)
|
||||
.extracting("reactivePulsarClient", InstanceOfAssertFactories.type(ReactivePulsarClient.class))
|
||||
.isSameAs(client);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
<T> void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.consumer.name=fromPropsCustomizer")
|
||||
.withUserConfiguration(ReactiveMessageConsumerBuilderCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
DefaultReactivePulsarConsumerFactory<?> consumerFactory = context
|
||||
.getBean(DefaultReactivePulsarConsumerFactory.class);
|
||||
Customizers<ReactiveMessageConsumerBuilderCustomizer<T>, ReactiveMessageConsumerBuilder<T>> customizers = Customizers
|
||||
.of(ReactiveMessageConsumerBuilder.class, ReactiveMessageConsumerBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(consumerFactory, "defaultConfigCustomizers")).callsInOrder(
|
||||
ReactiveMessageConsumerBuilder::consumerName, "fromPropsCustomizer", "fromCustomizer1",
|
||||
"fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ReactiveMessageConsumerBuilderCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ReactiveMessageConsumerBuilderCustomizer<?> customizerFoo() {
|
||||
return (builder) -> builder.consumerName("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ReactiveMessageConsumerBuilderCustomizer<?> customizerBar() {
|
||||
return (builder) -> builder.consumerName("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ListenerTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedBeanDoesNotAutoConfigureBean() {
|
||||
ReactivePulsarListenerContainerFactory<?> listenerContainerFactory = mock(
|
||||
ReactivePulsarListenerContainerFactory.class);
|
||||
this.contextRunner
|
||||
.withBean("reactivePulsarListenerContainerFactory", ReactivePulsarListenerContainerFactory.class,
|
||||
() -> listenerContainerFactory)
|
||||
.run((context) -> assertThat(context).getBean(ReactivePulsarListenerContainerFactory.class)
|
||||
.isSameAs(listenerContainerFactory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedReactivePulsarListenerAnnotationBeanPostProcessorDoesNotAutoConfigureBean() {
|
||||
ReactivePulsarListenerAnnotationBeanPostProcessor<?> listenerAnnotationBeanPostProcessor = mock(
|
||||
ReactivePulsarListenerAnnotationBeanPostProcessor.class);
|
||||
this.contextRunner.withBean(INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR,
|
||||
ReactivePulsarListenerAnnotationBeanPostProcessor.class, () -> listenerAnnotationBeanPostProcessor)
|
||||
.run((context) -> assertThat(context).getBean(ReactivePulsarListenerAnnotationBeanPostProcessor.class)
|
||||
.isSameAs(listenerAnnotationBeanPostProcessor));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasCustomProperties() {
|
||||
List<String> properties = new ArrayList<>();
|
||||
properties.add("spring.pulsar.listener.schema-type=avro");
|
||||
this.contextRunner.withPropertyValues(properties.toArray(String[]::new)).run((context) -> {
|
||||
DefaultReactivePulsarListenerContainerFactory<?> factory = context
|
||||
.getBean(DefaultReactivePulsarListenerContainerFactory.class);
|
||||
assertThat(factory.getContainerProperties().getSchemaType()).isEqualTo(SchemaType.AVRO);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
ReactivePulsarConsumerFactory<?> consumerFactory = mock(ReactivePulsarConsumerFactory.class);
|
||||
SchemaResolver schemaResolver = mock(SchemaResolver.class);
|
||||
this.contextRunner
|
||||
.withBean("customReactivePulsarConsumerFactory", ReactivePulsarConsumerFactory.class,
|
||||
() -> consumerFactory)
|
||||
.withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver)
|
||||
.run((context) -> {
|
||||
DefaultReactivePulsarListenerContainerFactory<?> containerFactory = context
|
||||
.getBean(DefaultReactivePulsarListenerContainerFactory.class);
|
||||
assertThat(containerFactory).extracting("consumerFactory").isSameAs(consumerFactory);
|
||||
assertThat(containerFactory)
|
||||
.extracting(DefaultReactivePulsarListenerContainerFactory::getContainerProperties)
|
||||
.extracting(ReactivePulsarContainerProperties::getSchemaResolver)
|
||||
.isSameAs(schemaResolver);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReaderFactoryTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void injectsExpectedBeans() {
|
||||
ReactivePulsarClient client = mock(ReactivePulsarClient.class);
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.reader.name=test-reader")
|
||||
.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client)
|
||||
.run((context) -> {
|
||||
DefaultReactivePulsarReaderFactory<?> readerFactory = context
|
||||
.getBean(DefaultReactivePulsarReaderFactory.class);
|
||||
assertThat(readerFactory)
|
||||
.extracting("reactivePulsarClient", InstanceOfAssertFactories.type(ReactivePulsarClient.class))
|
||||
.isSameAs(client);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
<T> void whenHasUserDefinedCustomizersAppliesInCorrectOrder() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.reader.name=fromPropsCustomizer")
|
||||
.withUserConfiguration(ReactiveMessageReaderBuilderCustomizerConfig.class)
|
||||
.run((context) -> {
|
||||
DefaultReactivePulsarReaderFactory<?> readerFactory = context
|
||||
.getBean(DefaultReactivePulsarReaderFactory.class);
|
||||
Customizers<ReactiveMessageReaderBuilderCustomizer<T>, ReactiveMessageReaderBuilder<T>> customizers = Customizers
|
||||
.of(ReactiveMessageReaderBuilder.class, ReactiveMessageReaderBuilderCustomizer::customize);
|
||||
assertThat(customizers.fromField(readerFactory, "defaultConfigCustomizers")).callsInOrder(
|
||||
ReactiveMessageReaderBuilder::readerName, "fromPropsCustomizer", "fromCustomizer1",
|
||||
"fromCustomizer2");
|
||||
});
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ReactiveMessageReaderBuilderCustomizerConfig {
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
ReactiveMessageReaderBuilderCustomizer<?> customizerFoo() {
|
||||
return (builder) -> builder.readerName("fromCustomizer2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
ReactiveMessageReaderBuilderCustomizer<?> customizerBar() {
|
||||
return (builder) -> builder.readerName("fromCustomizer1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class SenderCacheAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner;
|
||||
|
||||
@Test
|
||||
void whenNoPropertiesEnablesCaching() {
|
||||
this.contextRunner.run(this::assertCaffeineProducerCacheProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingEnabledEnablesCaching() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
|
||||
.run(this::assertCaffeineProducerCacheProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingDisabledDoesNotEnableCaching() {
|
||||
this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ProducerCacheProvider.class)
|
||||
.doesNotHaveBean(ReactiveMessageSenderCache.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingEnabledAndCaffeineNotOnClasspathStillUsesCaffeine() {
|
||||
// The reactive client shades Caffeine - it should still be used
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(Caffeine.class))
|
||||
.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
|
||||
.run(this::assertCaffeineProducerCacheProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCachingEnabledAndNoCacheProviderAvailable() {
|
||||
// The reactive client uses a shaded caffeine cache provider as its internal
|
||||
// cache
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(CaffeineShadedProducerCacheProvider.class))
|
||||
.withPropertyValues("spring.pulsar.producer.cache.enabled=true")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ProducerCacheProvider.class)
|
||||
.getBean(ReactiveMessageSenderCache.class)
|
||||
.extracting("cacheProvider")
|
||||
.isExactlyInstanceOf(CaffeineShadedProducerCacheProvider.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomCachingPropertiesCreatesConfiguredBean() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.pulsar.producer.cache.expire-after-access=100s",
|
||||
"spring.pulsar.producer.cache.maximum-size=5150",
|
||||
"spring.pulsar.producer.cache.initial-capacity=200")
|
||||
.run((context) -> assertCaffeineProducerCacheProvider(context).extracting("cache.cache")
|
||||
.hasFieldOrPropertyWithValue("expiresAfterAccessNanos", Duration.ofSeconds(100).toNanos())
|
||||
.hasFieldOrPropertyWithValue("maximum", 5150L));
|
||||
}
|
||||
|
||||
private AbstractObjectAssert<?, ProducerCacheProvider> assertCaffeineProducerCacheProvider(
|
||||
AssertableApplicationContext context) {
|
||||
return assertThat(context).hasSingleBean(ReactiveMessageSenderCache.class)
|
||||
.getBean(ProducerCacheProvider.class)
|
||||
.isExactlyInstanceOf(CaffeineShadedProducerCacheProvider.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pulsar.client.api.CompressionType;
|
||||
import org.apache.pulsar.client.api.DeadLetterPolicy;
|
||||
import org.apache.pulsar.client.api.HashingScheme;
|
||||
import org.apache.pulsar.client.api.MessageRoutingMode;
|
||||
import org.apache.pulsar.client.api.ProducerAccessMode;
|
||||
import org.apache.pulsar.client.api.RegexSubscriptionMode;
|
||||
import org.apache.pulsar.client.api.SubscriptionInitialPosition;
|
||||
import org.apache.pulsar.client.api.SubscriptionMode;
|
||||
import org.apache.pulsar.client.api.SubscriptionType;
|
||||
import org.apache.pulsar.common.schema.SchemaType;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder;
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer;
|
||||
import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer.Subscription;
|
||||
import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarReactivePropertiesMapper}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PulsarReactivePropertiesMapperTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customizeMessageSenderBuilder() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getProducer().setName("name");
|
||||
properties.getProducer().setTopicName("topicname");
|
||||
properties.getProducer().setSendTimeout(Duration.ofSeconds(1));
|
||||
properties.getProducer().setMessageRoutingMode(MessageRoutingMode.RoundRobinPartition);
|
||||
properties.getProducer().setHashingScheme(HashingScheme.JavaStringHash);
|
||||
properties.getProducer().setBatchingEnabled(false);
|
||||
properties.getProducer().setChunkingEnabled(true);
|
||||
properties.getProducer().setCompressionType(CompressionType.SNAPPY);
|
||||
properties.getProducer().setAccessMode(ProducerAccessMode.Exclusive);
|
||||
ReactiveMessageSenderBuilder<Object> builder = mock(ReactiveMessageSenderBuilder.class);
|
||||
new PulsarReactivePropertiesMapper(properties).customizeMessageSenderBuilder(builder);
|
||||
then(builder).should().producerName("name");
|
||||
then(builder).should().topic("topicname");
|
||||
then(builder).should().sendTimeout(Duration.ofSeconds(1));
|
||||
then(builder).should().messageRoutingMode(MessageRoutingMode.RoundRobinPartition);
|
||||
then(builder).should().hashingScheme(HashingScheme.JavaStringHash);
|
||||
then(builder).should().batchingEnabled(false);
|
||||
then(builder).should().chunkingEnabled(true);
|
||||
then(builder).should().compressionType(CompressionType.SNAPPY);
|
||||
then(builder).should().accessMode(ProducerAccessMode.Exclusive);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customizeMessageConsumerBuilder() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
List<String> topics = List.of("mytopic");
|
||||
Pattern topisPattern = Pattern.compile("my-pattern");
|
||||
properties.getConsumer().setName("name");
|
||||
properties.getConsumer().setTopics(topics);
|
||||
properties.getConsumer().setTopicsPattern(topisPattern);
|
||||
properties.getConsumer().setPriorityLevel(123);
|
||||
properties.getConsumer().setReadCompacted(true);
|
||||
Consumer.DeadLetterPolicy deadLetterPolicy = new Consumer.DeadLetterPolicy();
|
||||
deadLetterPolicy.setDeadLetterTopic("my-dlt");
|
||||
deadLetterPolicy.setMaxRedeliverCount(1);
|
||||
properties.getConsumer().setDeadLetterPolicy(deadLetterPolicy);
|
||||
properties.getConsumer().setRetryEnable(false);
|
||||
Subscription subscriptionProperties = properties.getConsumer().getSubscription();
|
||||
subscriptionProperties.setName("subname");
|
||||
subscriptionProperties.setInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
subscriptionProperties.setMode(SubscriptionMode.NonDurable);
|
||||
subscriptionProperties.setTopicsMode(RegexSubscriptionMode.NonPersistentOnly);
|
||||
subscriptionProperties.setType(SubscriptionType.Key_Shared);
|
||||
ReactiveMessageConsumerBuilder<Object> builder = mock(ReactiveMessageConsumerBuilder.class);
|
||||
new PulsarReactivePropertiesMapper(properties).customizeMessageConsumerBuilder(builder);
|
||||
then(builder).should().consumerName("name");
|
||||
then(builder).should().topics(topics);
|
||||
then(builder).should().topicsPattern(topisPattern);
|
||||
then(builder).should().priorityLevel(123);
|
||||
then(builder).should().readCompacted(true);
|
||||
then(builder).should().deadLetterPolicy(new DeadLetterPolicy(1, null, "my-dlt", null));
|
||||
then(builder).should().retryLetterTopicEnable(false);
|
||||
then(builder).should().subscriptionName("subname");
|
||||
then(builder).should().subscriptionInitialPosition(SubscriptionInitialPosition.Earliest);
|
||||
then(builder).should().subscriptionMode(SubscriptionMode.NonDurable);
|
||||
then(builder).should().topicsPatternSubscriptionMode(RegexSubscriptionMode.NonPersistentOnly);
|
||||
then(builder).should().subscriptionType(SubscriptionType.Key_Shared);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeContainerProperties() {
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getConsumer().getSubscription().setType(SubscriptionType.Shared);
|
||||
properties.getListener().setSchemaType(SchemaType.AVRO);
|
||||
ReactivePulsarContainerProperties<Object> containerProperties = new ReactivePulsarContainerProperties<>();
|
||||
new PulsarReactivePropertiesMapper(properties).customizeContainerProperties(containerProperties);
|
||||
assertThat(containerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared);
|
||||
assertThat(containerProperties.getSchemaType()).isEqualTo(SchemaType.AVRO);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customizeMessageReaderBuilder() {
|
||||
List<String> topics = List.of("my-topic");
|
||||
PulsarProperties properties = new PulsarProperties();
|
||||
properties.getReader().setName("name");
|
||||
properties.getReader().setTopics(topics);
|
||||
properties.getReader().setSubscriptionName("subname");
|
||||
properties.getReader().setSubscriptionRolePrefix("srp");
|
||||
ReactiveMessageReaderBuilder<Object> builder = mock(ReactiveMessageReaderBuilder.class);
|
||||
new PulsarReactivePropertiesMapper(properties).customizeMessageReaderBuilder(builder);
|
||||
then(builder).should().readerName("name");
|
||||
then(builder).should().topics(topics);
|
||||
then(builder).should().subscriptionName("subname");
|
||||
then(builder).should().generatedSubscriptionNamePrefix("srp");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
[[messaging.pulsar]]
|
||||
== Apache Pulsar Support
|
||||
https://pulsar.apache.org/[Apache Pulsar] is supported by providing auto-configuration of the {spring-pulsar-docs}[Spring for Apache Pulsar] project.
|
||||
|
||||
Spring Boot will auto-configure and register the classic (imperative) Spring Pulsar components when `org.springframework.pulsar:spring-pulsar` is on the classpath.
|
||||
It will do the same for the reactive components when `org.springframework.pulsar:spring-pulsar-reactive` is on the classpath.
|
||||
|
||||
There are `spring-boot-starter-pulsar` and `spring-boot-starter-pulsar-reactive` "`Starters`" for conveniently collecting the dependencies for imperative and reactive use, respectively.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.connecting]]
|
||||
=== Connecting to Pulsar
|
||||
When you use the Pulsar starter, Spring Boot will auto-configure and register a `PulsarClient` bean.
|
||||
|
||||
By default, the application tries to connect to a local Pulsar instance at `pulsar://localhost:6650`.
|
||||
This can be adjusted by setting the configprop:spring.pulsar.client.service-url[] property to a different value.
|
||||
|
||||
NOTE: The value must be a valid https://pulsar.apache.org/docs/client-libraries-java/#connection-urls[Pulsar Protocol] URL
|
||||
|
||||
You can configure the client by specifying any of the `spring.pulsar.client.*` prefixed application properties.
|
||||
|
||||
If you need more control over the configuration, consider registering one or more `PulsarClientBuilderCustomizer` beans.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.connecting.auth]]
|
||||
==== Authentication
|
||||
To connect to a Pulsar cluster that requires authentication, you need to specify which authentication plugin to use by setting the `authPluginClassName` and any parameters required by the plugin.
|
||||
You can set the parameters as a map of parameter names to parameter values.
|
||||
The following example shows how to configure the `AuthenticationOAuth2` plugin.
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
|
||||
----
|
||||
spring:
|
||||
pulsar:
|
||||
client:
|
||||
authentication:
|
||||
plugin-class-name: org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2
|
||||
param:
|
||||
issuerUrl: https://auth.server.cloud/
|
||||
privateKey: file:///Users/some-key.json
|
||||
audience: urn:sn:acme:dev:my-instance
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
You need to ensure that names defined under `+spring.pulsar.client.authentication.param.*+` exactly match those expected by your auth plugin (which is typically camel cased).
|
||||
Spring Boot will not attempt any kind of relaxed binding for these entries.
|
||||
|
||||
For example, if you want to configure the issuer url for the `AuthenticationOAuth2` auth plugin you must use `+spring.pulsar.client.authentication.param.issuerUrl+`.
|
||||
If you use other forms, such as `issuerurl` or `issuer-url`, the setting will not be applied to the plugin.
|
||||
====
|
||||
|
||||
For complete details on the client and authentication see the Spring Pulsar {spring-pulsar-docs}#pulsar-client[reference documentation].
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.connecting-reactive]]
|
||||
=== Connecting to Pulsar Reactively
|
||||
When the Reactive auto-configuration is activated, Spring Boot will auto-configure and register a `ReactivePulsarClient` bean.
|
||||
|
||||
The `ReactivePulsarClient` adapts an instance of the previously described `PulsarClient`.
|
||||
Therefore, follow the previous section to configure the `PulsarClient` used by the `ReactivePulsarClient`.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.admin]]
|
||||
=== Connecting to Pulsar Administration
|
||||
Spring Pulsar's `PulsarAdministration` client is also auto-configured.
|
||||
|
||||
By default, the application tries to connect to a local Pulsar instance at `\http://localhost:8080`.
|
||||
This can be adjusted by setting the configprop:spring.pulsar.admin.service-url[] property to a different value in the form `(http|https)://<host>:<port>`.
|
||||
|
||||
If you need more control over the configuration, consider registering one or more `PulsarAdminBuilderCustomizer` beans.
|
||||
|
||||
|
||||
[[messaging.pulsar.admin.auth]]
|
||||
==== Authentication
|
||||
When accessing a Pulsar cluster that requires authentication, the admin client requires the same security configuration as the regular Pulsar client.
|
||||
You can use the aforementioned <<pulsar.adoc#messaging.pulsar.connecting.auth,authentication configuration>> by replacing `spring.pulsar.client.authentication` with `spring.pulsar.admin.authentication`.
|
||||
|
||||
TIP: To create a topic on startup, add a bean of type `PulsarTopic`.
|
||||
If the topic already exists, the bean is ignored.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.sending]]
|
||||
=== Sending a Message
|
||||
Spring's `PulsarTemplate` is auto-configured, and you can use it to send messages, as shown in the following example:
|
||||
|
||||
include::code:MyBean[]
|
||||
|
||||
The `PulsarTemplate` relies on a `PulsarProducerFactory` to create the underlying Pulsar producer.
|
||||
Spring Boot auto-configuration also provides this producer factory, which by default, caches the producers that it creates.
|
||||
You can configure the producer factory and cache settings by specifying any of the `spring.pulsar.producer.\*` and `spring.pulsar.producer.cache.*` prefixed application properties.
|
||||
|
||||
If you need more control over the producer factory configuration, consider registering one or more `ProducerBuilderCustomizer` beans.
|
||||
These customizers are applied to all created producers.
|
||||
You can also pass in a `ProducerBuilderCustomizer` when sending a message to only affect the current producer.
|
||||
|
||||
If you need more control over the message being sent, you can pass in a `TypedMessageBuilderCustomizer` when sending a message.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.sending-reactive]]
|
||||
=== Sending a Message Reactively
|
||||
When the Reactive auto-configuration is activated, Spring's `ReactivePulsarTemplate` is auto-configured, and you can use it to send messages, as shown in the following example:
|
||||
|
||||
include::code:MyBean[]
|
||||
|
||||
The `ReactivePulsarTemplate` relies on a `ReactivePulsarSenderFactory` to actually create the underlying sender.
|
||||
Spring Boot auto-configuration also provides this sender factory, which by default, caches the producers that it creates.
|
||||
You can configure the sender factory and cache settings by specifying any of the `spring.pulsar.producer.\*` and `spring.pulsar.producer.cache.*` prefixed application properties.
|
||||
|
||||
If you need more control over the sender factory configuration, consider registering one or more `ReactiveMessageSenderBuilderCustomizer` beans.
|
||||
These customizers are applied to all created senders.
|
||||
You can also pass in a `ReactiveMessageSenderBuilderCustomizer` when sending a message to only affect the current sender.
|
||||
|
||||
If you need more control over the message being sent, you can pass in a `MessageSpecBuilderCustomizer` when sending a message.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.receiving]]
|
||||
=== Receiving a Message
|
||||
When the Apache Pulsar infrastructure is present, any bean can be annotated with `@PulsarListener` to create a listener endpoint.
|
||||
The following component creates a listener endpoint on the `someTopic` topic:
|
||||
|
||||
include::code:MyBean[]
|
||||
|
||||
Spring Boot auto-configuration provides all the components necessary for `PulsarListener`, such as the `PulsarListenerContainerFactory` and the consumer factory it uses to construct the underlying Pulsar consumers.
|
||||
You can configure these components by specifying any of the `spring.pulsar.listener.\*` and `spring.pulsar.consumer.*` prefixed application properties.
|
||||
|
||||
If you need more control over the consumer factory configuration, consider registering one or more `ConsumerBuilderCustomizer` beans.
|
||||
These customizers are applied to all consumers created by the factory, and therefore all `@PulsarListener` instances.
|
||||
You can also customize a single listener by setting the `consumerCustomizer` attribute of the `@PulsarListener` annotation.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.receiving-reactive]]
|
||||
=== Receiving a Message Reactively
|
||||
When the Apache Pulsar infrastructure is present and the Reactive auto-configuration is activated, any bean can be annotated with `@ReactivePulsarListener` to create a reactive listener endpoint.
|
||||
The following component creates a reactive listener endpoint on the `someTopic` topic:
|
||||
|
||||
include::code:MyBean[]
|
||||
|
||||
Spring Boot auto-configuration provides all the components necessary for `ReactivePulsarListener`, such as the `ReactivePulsarListenerContainerFactory` and the consumer factory it uses to construct the underlying reactive Pulsar consumers.
|
||||
You can configure these components by specifying any of the `spring.pulsar.listener.*` and `spring.pulsar.consumer.*` prefixed application properties.
|
||||
|
||||
If you need more control over the consumer factory configuration, consider registering one or more `ReactiveMessageConsumerBuilderCustomizer` beans.
|
||||
These customizers are applied to all consumers created by the factory, and therefore all `@ReactivePulsarListener` instances.
|
||||
You can also customize a single listener by setting the `consumerCustomizer` attribute of the `@ReactivePulsarListener` annotation.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.reading]]
|
||||
=== Reading a Message
|
||||
The Pulsar reader interface enables applications to manually manage cursors.
|
||||
When you use a reader to connect to a topic you need to specify which message the reader begins reading from when it connects to a topic.
|
||||
|
||||
When the Apache Pulsar infrastructure is present, any bean can be annotated with `@PulsarReader` to consume messages using a reader.
|
||||
The following component creates a reader endpoint that starts reading messages from the beginning of the `someTopic` topic:
|
||||
|
||||
include::code:MyBean[]
|
||||
|
||||
The `@PulsarReader` relies on a `PulsarReaderFactory` to create the underlying Pulsar reader.
|
||||
Spring Boot auto-configuration provides this reader factory which can be customized by setting any of the `spring.pulsar.reader.*` prefixed application properties.
|
||||
|
||||
If you need more control over the reader factory configuration, consider registering one or more `ReaderBuilderCustomizer` beans.
|
||||
These customizers are applied to all readers created by the factory, and therefore all `@PulsarReader` instances.
|
||||
You can also customize a single listener by setting the `readerCustomizer` attribute of the `@PulsarReader` annotation.
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.reading-reactive]]
|
||||
=== Reading a Message Reactively
|
||||
When the Apache Pulsar infrastructure is present and the Reactive auto-configuration is activated, Spring's `ReactivePulsarReaderFactory` is provided, and you can use it to create a reader in order to read messages in a reactive fashion.
|
||||
The following component creates a reader using the provided factory and reads a single message from 5 minutes ago from the `someTopic` topic:
|
||||
|
||||
include::code:MyBean[]
|
||||
|
||||
Spring Boot auto-configuration provides this reader factory which can be customized by setting any of the `spring.pulsar.reader.*` prefixed application properties.
|
||||
|
||||
If you need more control over the reader factory configuration, consider passing in one or more `ReactiveMessageReaderBuilderCustomizer` instances when using the factory to create a reader.
|
||||
|
||||
If you need more control over the reader factory configuration, consider registering one or more `ReactiveMessageReaderBuilderCustomizer` beans.
|
||||
These customizers are applied to all created readers.
|
||||
You can also pass one or more `ReactiveMessageReaderBuilderCustomizer` when creating a reader to only apply the customizations to the created reader.
|
||||
|
||||
TIP: For more details on any of the above components and to discover other available features, see the Spring for Apache Pulsar {spring-pulsar-docs}[reference documentation].
|
||||
|
||||
|
||||
|
||||
[[messaging.pulsar.additional-properties]]
|
||||
=== Additional Pulsar Properties
|
||||
The properties supported by auto-configuration are shown in the <<application-properties#appendix.application-properties.integration, "`Integration Properties`">> section of the Appendix.
|
||||
Note that, for the most part, these properties (hyphenated or camelCase) map directly to the Apache Pulsar configuration properties.
|
||||
See the Apache Pulsar documentation for details.
|
||||
|
||||
Only a subset of the properties supported by Pulsar are available directly through the `PulsarProperties` class.
|
||||
If you wish to tune the auto-configured components with additional properties that are not directly supported, you can use the customizer supported by each aforementioned component.
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.reading;
|
||||
|
||||
import org.springframework.pulsar.annotation.PulsarReader;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
@PulsarReader(topics = "someTopic", startMessageId = "earliest")
|
||||
public void processMessage(String content) {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.readingreactive;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.reactive.client.api.StartAtSpec;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
private final ReactivePulsarReaderFactory<String> pulsarReaderFactory;
|
||||
|
||||
public MyBean(ReactivePulsarReaderFactory<String> pulsarReaderFactory) {
|
||||
this.pulsarReaderFactory = pulsarReaderFactory;
|
||||
}
|
||||
|
||||
public void someMethod() {
|
||||
ReactiveMessageReaderBuilderCustomizer<String> readerBuilderCustomizer = (readerBuilder) -> readerBuilder
|
||||
.topic("someTopic")
|
||||
.startAtSpec(StartAtSpec.ofInstant(Instant.now().minusSeconds(5)));
|
||||
Mono<Message<String>> message = this.pulsarReaderFactory
|
||||
.createReader(Schema.STRING, List.of(readerBuilderCustomizer))
|
||||
.readOne();
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.receiving;
|
||||
|
||||
import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
@PulsarListener(topics = "someTopic")
|
||||
public void processMessage(String content) {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.receivingreactive;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
@ReactivePulsarListener(topics = "someTopic")
|
||||
public Mono<Void> processMessage(String content) {
|
||||
// ...
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.sending;
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClientException;
|
||||
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
private final PulsarTemplate<String> pulsarTemplate;
|
||||
|
||||
public MyBean(PulsarTemplate<String> pulsarTemplate) {
|
||||
this.pulsarTemplate = pulsarTemplate;
|
||||
}
|
||||
|
||||
public void someMethod() throws PulsarClientException {
|
||||
this.pulsarTemplate.send("someTopic", "Hello");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.sendingreactive;
|
||||
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
private final ReactivePulsarTemplate<String> pulsarTemplate;
|
||||
|
||||
public MyBean(ReactivePulsarTemplate<String> pulsarTemplate) {
|
||||
this.pulsarTemplate = pulsarTemplate;
|
||||
}
|
||||
|
||||
public void someMethod() {
|
||||
this.pulsarTemplate.send("someTopic", "Hello").subscribe();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.reading
|
||||
|
||||
import org.springframework.pulsar.annotation.PulsarReader
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
@Component
|
||||
class MyBean {
|
||||
|
||||
@PulsarReader(topics = ["someTopic"], startMessageId = "earliest")
|
||||
fun processMessage(content: String?) {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.readingreactive
|
||||
|
||||
import org.apache.pulsar.client.api.Schema
|
||||
import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder
|
||||
import org.apache.pulsar.reactive.client.api.StartAtSpec
|
||||
import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Instant
|
||||
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
|
||||
@Component
|
||||
class MyBean(private val pulsarReaderFactory: ReactivePulsarReaderFactory<String>) {
|
||||
|
||||
fun someMethod() {
|
||||
val readerBuilderCustomizer = ReactiveMessageReaderBuilderCustomizer {
|
||||
readerBuilder: ReactiveMessageReaderBuilder<String> ->
|
||||
readerBuilder
|
||||
.topic("someTopic")
|
||||
.startAtSpec(StartAtSpec.ofInstant(Instant.now().minusSeconds(5)))
|
||||
}
|
||||
val message = pulsarReaderFactory
|
||||
.createReader(Schema.STRING, listOf(readerBuilderCustomizer))
|
||||
.readOne()
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.receiving
|
||||
|
||||
import org.springframework.pulsar.annotation.PulsarListener
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
@Component
|
||||
class MyBean {
|
||||
|
||||
@PulsarListener(topics = ["someTopic"])
|
||||
fun processMessage(content: String?) {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.boot.docs.messaging.pulsar.receivingreactive
|
||||
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Component
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
class MyBean {
|
||||
|
||||
@ReactivePulsarListener(topics = ["someTopic"])
|
||||
fun processMessage(content: String?): Mono<Void> {
|
||||
// ...
|
||||
return Mono.empty()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.messaging.pulsar.sending
|
||||
|
||||
import org.apache.pulsar.client.api.PulsarClientException
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.pulsar.core.PulsarTemplate
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class MyBean(private val pulsarTemplate: PulsarTemplate<String>) {
|
||||
|
||||
@Throws(PulsarClientException::class)
|
||||
fun someMethod() {
|
||||
pulsarTemplate.send("someTopic", "Hello")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.boot.docs.messaging.pulsar.sendingreactive
|
||||
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class MyBean(private val pulsarTemplate: ReactivePulsarTemplate<String>) {
|
||||
|
||||
fun someMethod() {
|
||||
pulsarTemplate.send("someTopic", "Hello").subscribe()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id "org.springframework.boot.starter"
|
||||
}
|
||||
|
||||
description = "Starter for using Spring for Apache Pulsar Reactive"
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
|
||||
api("org.springframework.pulsar:spring-pulsar-reactive")
|
||||
}
|
||||
|
||||
checkRuntimeClasspathForConflicts {
|
||||
ignore { name -> name.startsWith("org/bouncycastle/") ||
|
||||
name.matches("^org\\/apache\\/pulsar\\/.*\\/package-info.class\$") ||
|
||||
name.equals("findbugsExclude.xml") ||
|
||||
name.startsWith("org/springframework/pulsar/shade/com/github/benmanes/caffeine/") ||
|
||||
name.startsWith("org/springframework/pulsar/shade/com/google/errorprone/") ||
|
||||
name.startsWith("org/springframework/pulsar/shade/org/checkerframework/") }
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
id "org.springframework.boot.starter"
|
||||
}
|
||||
|
||||
description = "Starter for using Spring for Apache Pulsar"
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
|
||||
api("org.springframework.pulsar:spring-pulsar")
|
||||
}
|
||||
|
||||
checkRuntimeClasspathForConflicts {
|
||||
ignore { name -> name.startsWith("org/bouncycastle/") ||
|
||||
name.matches("^org\\/apache\\/pulsar\\/.*\\/package-info.class\$") ||
|
||||
name.equals("findbugsExclude.xml") }
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot.conventions"
|
||||
}
|
||||
|
||||
description = "Spring Boot Pulsar smoke test"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar-reactive"))
|
||||
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||
testImplementation("org.awaitility:awaitility")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
testImplementation("org.testcontainers:pulsar")
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar.reactive;
|
||||
|
||||
record SampleMessage(Integer id, String content) {
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar.reactive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
class SampleMessageConsumer {
|
||||
|
||||
private List<SampleMessage> consumed = new ArrayList<>();
|
||||
|
||||
List<SampleMessage> getConsumed() {
|
||||
return this.consumed;
|
||||
}
|
||||
|
||||
@ReactivePulsarListener(topics = SampleReactivePulsarApplication.TOPIC)
|
||||
Mono<Void> consumeMessagesFromPulsarTopic(SampleMessage msg) {
|
||||
System.out.println("**** CONSUME: " + msg);
|
||||
this.consumed.add(msg);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar.reactive;
|
||||
|
||||
import org.apache.pulsar.reactive.client.api.MessageSpec;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleReactivePulsarApplication {
|
||||
|
||||
static final String TOPIC = "pulsar-reactive-smoke-test-topic";
|
||||
|
||||
@Bean
|
||||
PulsarTopic pulsarTestTopic() {
|
||||
return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApplicationRunner sendMessagesToPulsarTopic(ReactivePulsarTemplate<SampleMessage> template) {
|
||||
return (args) -> Flux.range(0, 10)
|
||||
.map((i) -> new SampleMessage(i, "message:" + i))
|
||||
.map(MessageSpec::of)
|
||||
.as((msgs) -> template.send(TOPIC, msgs))
|
||||
.doOnNext((sendResult) -> System.out.println("*** PRODUCE: " + sendResult.getMessageId()))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleReactivePulsarApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
spring.pulsar.reactive.consumer.subscription-initial-position=earliest
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PulsarContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class SampleReactivePulsarApplicationTests {
|
||||
|
||||
@Container
|
||||
private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar())
|
||||
.withStartupAttempts(2)
|
||||
.withStartupTimeout(Duration.ofMinutes(3));
|
||||
|
||||
@DynamicPropertySource
|
||||
static void pulsarProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl);
|
||||
registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) {
|
||||
Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new);
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(20))
|
||||
.untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id)
|
||||
.containsExactly(expectedIds));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot.conventions"
|
||||
}
|
||||
|
||||
description = "Spring Boot Pulsar smoke test"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar"))
|
||||
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||
testImplementation("org.awaitility:awaitility")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
testImplementation("org.testcontainers:pulsar")
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar;
|
||||
|
||||
record SampleMessage(Integer id, String content) {
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.pulsar.annotation.PulsarListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
class SampleMessageConsumer {
|
||||
|
||||
private List<SampleMessage> consumed = new ArrayList<>();
|
||||
|
||||
List<SampleMessage> getConsumed() {
|
||||
return this.consumed;
|
||||
}
|
||||
|
||||
@PulsarListener(topics = SamplePulsarApplication.TOPIC)
|
||||
void consumeMessagesFromPulsarTopic(SampleMessage msg) {
|
||||
System.out.println("**** CONSUME: " + msg);
|
||||
this.consumed.add(msg);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar;
|
||||
|
||||
import org.apache.pulsar.client.api.MessageId;
|
||||
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
import org.springframework.pulsar.core.PulsarTopic;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SamplePulsarApplication {
|
||||
|
||||
static final String TOPIC = "pulsar-smoke-test-topic";
|
||||
|
||||
@Bean
|
||||
PulsarTopic pulsarTestTopic() {
|
||||
return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApplicationRunner sendMessagesToPulsarTopic(PulsarTemplate<SampleMessage> template) {
|
||||
return (args) -> {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
MessageId msgId = template.send(TOPIC, new SampleMessage(i, "message:" + i));
|
||||
System.out.println("*** PRODUCE: " + msgId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SamplePulsarApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
spring.pulsar.consumer.subscription-initial-position=earliest
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.pulsar;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PulsarContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class SamplePulsarApplicationTests {
|
||||
|
||||
@Container
|
||||
private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar())
|
||||
.withStartupAttempts(2)
|
||||
.withStartupTimeout(Duration.ofMinutes(3));
|
||||
|
||||
@DynamicPropertySource
|
||||
static void pulsarProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl);
|
||||
registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) {
|
||||
Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new);
|
||||
Awaitility.await()
|
||||
.atMost(Duration.ofSeconds(20))
|
||||
.untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id)
|
||||
.containsExactly(expectedIds));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue