├── src └── main │ ├── java │ └── io │ │ └── github │ │ └── howinfun │ │ ├── ececption │ │ ├── PulsarBusinessException.java │ │ └── PulsarAutoConfigException.java │ │ ├── constant │ │ └── PulsarConstant.java │ │ ├── configuration │ │ ├── EnablePulsar.java │ │ ├── PulsarAutoConfiguration.java │ │ └── PulsarConsumerAutoConfigure.java │ │ ├── properties │ │ ├── PulsarProperties.java │ │ └── MultiPulsarProperties.java │ │ ├── listener │ │ ├── ThreadPool.java │ │ ├── PulsarListener.java │ │ └── BaseMessageListener.java │ │ ├── utils │ │ └── TopicUtil.java │ │ ├── client │ │ └── MultiPulsarClient.java │ │ └── template │ │ └── PulsarTemplate.java │ └── resources │ └── META-INF │ └── spring-configuration-metadata.json ├── LICENSE ├── .gitignore ├── pom.xml └── README.md /src/main/java/io/github/howinfun/ececption/PulsarBusinessException.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.ececption; 2 | 3 | /** 4 | * 自定义Pulsar异常 5 | * @author winfun 6 | **/ 7 | public class PulsarBusinessException extends RuntimeException{ 8 | 9 | public PulsarBusinessException(String msg){ 10 | super(msg); 11 | } 12 | 13 | public PulsarBusinessException(String msg,Throwable e){ 14 | super(msg,e); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/ececption/PulsarAutoConfigException.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.ececption; 2 | 3 | /** 4 | * 自定义Pulsar异常 5 | * @author winfun 6 | **/ 7 | public class PulsarAutoConfigException extends RuntimeException{ 8 | 9 | public PulsarAutoConfigException(String msg){ 10 | super(msg); 11 | } 12 | 13 | public PulsarAutoConfigException(String msg,Throwable e){ 14 | super(msg,e); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/constant/PulsarConstant.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.constant; 2 | 3 | /** 4 | * pulsar常量 5 | * @author winfun 6 | **/ 7 | public final class PulsarConstant { 8 | 9 | private PulsarConstant(){} 10 | 11 | /** 12 | * 间隔符 13 | */ 14 | public static final String PATH_SPLIT = "/"; 15 | /** 16 | * 持久化 17 | */ 18 | public static final String PERSISTENT = "persistent://"; 19 | /** 20 | * 非持久化 21 | */ 22 | public static final String NON_PERSISTENT = "non-persistent://"; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/configuration/EnablePulsar.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.configuration; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Inherited; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 开启pulsar自动配置,包括Producer和Consumer 13 | * @author winfun 14 | **/ 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Target(ElementType.TYPE) 17 | @Inherited 18 | @Component 19 | @Import({PulsarAutoConfiguration.class}) 20 | public @interface EnablePulsar { 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/properties/PulsarProperties.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.properties; 2 | 3 | /** 4 | * Pulsar配置类 5 | * @author winfun 6 | * @deprecated 支持多数据源注入 7 | **/ 8 | @Deprecated 9 | public class PulsarProperties { 10 | 11 | /** 12 | * pulsar服务地址 13 | */ 14 | private String serviceUrl; 15 | /** 16 | * 租户 17 | */ 18 | private String tenant; 19 | /** 20 | * 命名空间 21 | */ 22 | private String namespace; 23 | /** 24 | * 是否开启TCP不延迟 25 | */ 26 | private Boolean enableTcpNoDelay=true; 27 | /** 28 | * 操作超时,单位秒 29 | */ 30 | private Integer operationTimeout=30; 31 | /** 32 | * 消费者监听线程数 33 | */ 34 | private Integer listenerThreads=1; 35 | /** 36 | * IO线程数 37 | */ 38 | private Integer ioThreads=1; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/listener/ThreadPool.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.listener; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | import java.lang.annotation.Target; 6 | 7 | /** 8 | * 自定义注解 9 | * Consumer线程池参数 10 | * @author winfun 11 | **/ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({}) 14 | public @interface ThreadPool { 15 | 16 | /** 17 | * 线程名称 18 | */ 19 | String threadPoolName() default "base-message-listener-execute"; 20 | /** 21 | * 核心线程数,默认1 22 | */ 23 | int coreThreads() default 1; 24 | /** 25 | * 核心线程数,默认1 26 | */ 27 | int maxCoreThreads() default 1; 28 | /** 29 | * 线程活跃时长,单位分钟,默认10 30 | */ 31 | int keepAliveTime() default 10; 32 | /** 33 | * 最大等待队列长度,默认100 34 | */ 35 | int maxQueueLength() default 100; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/utils/TopicUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.utils; 2 | 3 | import io.github.howinfun.constant.PulsarConstant; 4 | import java.util.StringJoiner; 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.NotNull; 7 | 8 | /** 9 | * 主题工具类 10 | * @author winfun 11 | **/ 12 | public final class TopicUtil { 13 | 14 | private TopicUtil(){} 15 | 16 | /** 17 | * 拼接topic 18 | * @return 完整topic路径 19 | */ 20 | public static String generateTopic(@NotNull Boolean persistent, @NotBlank String tenant, @NotBlank String namespace, @NotBlank String topic){ 21 | 22 | StringJoiner stringJoiner = new StringJoiner(PulsarConstant.PATH_SPLIT); 23 | stringJoiner.add(tenant).add(namespace).add(topic); 24 | if (Boolean.TRUE.equals(persistent)){ 25 | return PulsarConstant.PERSISTENT + stringJoiner.toString(); 26 | }else { 27 | return PulsarConstant.NON_PERSISTENT + stringJoiner.toString(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Howinfun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 5 | "name": "pulsar.serviceUrl", 6 | "type": "java.lang.String", 7 | "description": "服务地址。" 8 | }, 9 | { 10 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 11 | "name": "pulsar.tenant", 12 | "type": "java.lang.String", 13 | "description": "租户。" 14 | }, 15 | { 16 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 17 | "name": "pulsar.namespace", 18 | "type": "java.lang.String", 19 | "description": "命名空间。" 20 | }, 21 | { 22 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 23 | "name": "pulsar.enableTcpNoDelay", 24 | "type": "java.lang.Boolean", 25 | "description": "是否开启TCP无延迟。" 26 | }, 27 | { 28 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 29 | "name": "pulsar.operationTimeout", 30 | "type": "java.lang.Integer", 31 | "description": "操作超时时长,单位秒。" 32 | }, 33 | { 34 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 35 | "name": "pulsar.listenerThreads", 36 | "type": "java.lang.Integer", 37 | "description": "监听消息线程数,默认1。" 38 | }, 39 | { 40 | "sourceType": "io.github.howinfun.properties.PulsarProperties", 41 | "name": "pulsar.ioThreads", 42 | "type": "java.lang.Integer", 43 | "description": "IO线程数,默认1。" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/configuration/PulsarAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.configuration; 2 | 3 | import io.github.howinfun.client.MultiPulsarClient; 4 | import io.github.howinfun.properties.MultiPulsarProperties; 5 | import io.github.howinfun.template.PulsarTemplate; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | /** 14 | * pulsar 自动配置 15 | * @author winfun 16 | **/ 17 | @Slf4j 18 | @Configuration 19 | @ComponentScan("io.github.howinfun") 20 | @ConditionalOnExpression("!'${pulsar.serviceUrl}'.isEmpty()") 21 | @EnableConfigurationProperties({MultiPulsarProperties.class}) 22 | public class PulsarAutoConfiguration { 23 | 24 | /** 25 | * 注入多数据源Pulsar客户端 26 | * @param multiPulsarProperties 多数据源pulsar自定义配置 27 | * @return 客户端 28 | */ 29 | @Bean 30 | public MultiPulsarClient multiPulsarClient(MultiPulsarProperties multiPulsarProperties){ 31 | return new MultiPulsarClient(multiPulsarProperties); 32 | } 33 | 34 | /** 35 | * 注入Pulsar Producer模版类 36 | * @param multiPulsarClient 多数据源Pulsar客户端 37 | * @param multiPulsarProperties pulsar自定义配置 38 | * @return 模版类 39 | */ 40 | @Bean 41 | public PulsarTemplate pulsarTemplate(MultiPulsarClient multiPulsarClient, MultiPulsarProperties multiPulsarProperties){ 42 | return new PulsarTemplate(multiPulsarClient,multiPulsarProperties); 43 | } 44 | 45 | /*** 46 | * 注入Pulsar Consumer自动配置类 47 | * @param multiPulsarClient multiPulsarClient 48 | * @param multiPulsarProperties multiPulsarProperties 49 | * @return Pulsar Consumer 自动配置类 50 | **/ 51 | @Bean 52 | public PulsarConsumerAutoConfigure pulsarConsumerAutoConfigure(MultiPulsarClient multiPulsarClient, MultiPulsarProperties multiPulsarProperties){ 53 | return new PulsarConsumerAutoConfigure(multiPulsarClient,multiPulsarProperties); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/listener/PulsarListener.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.listener; 2 | 3 | import io.github.howinfun.properties.MultiPulsarProperties; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Inherited; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | import org.apache.pulsar.client.api.Consumer; 10 | import org.apache.pulsar.client.api.DeadLetterPolicy; 11 | import org.apache.pulsar.client.api.Message; 12 | import org.apache.pulsar.client.api.SubscriptionType; 13 | import org.springframework.stereotype.Component; 14 | 15 | /** 16 | * 自定义注解 17 | * 初始化Consumer的配置参数 18 | * @author winfun 19 | **/ 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Target(ElementType.TYPE) 22 | @Inherited 23 | @Component 24 | public @interface PulsarListener { 25 | 26 | /** 27 | * 数据源名称,默认:"default" 28 | */ 29 | String sourceName() default MultiPulsarProperties.DEFAULT_SOURCE_NAME; 30 | /** 31 | * 是否持久化 32 | */ 33 | boolean persistent() default true; 34 | /** 35 | * 租户 36 | */ 37 | String tenant() default ""; 38 | /** 39 | * 命名空间 40 | */ 41 | String namespace() default ""; 42 | /** 43 | * 监听topic 44 | */ 45 | String[] topics() default {}; 46 | 47 | /** 48 | * 接收消息的队列大小 49 | */ 50 | int receiverQueueSize() default 1000; 51 | 52 | /** 53 | * 订阅名称 54 | */ 55 | String subscriptionName() default ""; 56 | 57 | /** 58 | * 订阅模式 59 | */ 60 | SubscriptionType subscriptionType() default SubscriptionType.Shared; 61 | 62 | /** 63 | * 应答超时事件,单位毫秒 64 | * @see Consumer#acknowledge(Message) 65 | */ 66 | String ackTimeout() default "1000"; 67 | 68 | /** 69 | * 重新投递时延,单位毫秒 70 | * @see Consumer#negativeAcknowledge(Message) 71 | */ 72 | String negativeAckRedeliveryDelay() default "1000"; 73 | 74 | /** 75 | * 是否开启重试,默认false 76 | */ 77 | boolean enableRetry() default false; 78 | 79 | /** 80 | * 最大重新投递次数,超过此次数,进入死信队列 81 | * @see DeadLetterPolicy#maxRedeliverCount 82 | */ 83 | int maxRedeliverCount() default 2; 84 | 85 | /** 86 | * 重试队列 87 | * @see DeadLetterPolicy#retryLetterTopic 88 | */ 89 | String retryLetterTopic() default ""; 90 | 91 | /** 92 | * 死信队列 93 | * @see DeadLetterPolicy#deadLetterTopic 94 | */ 95 | String deadLetterTopic() default ""; 96 | 97 | /** 98 | * 消费线程池 99 | */ 100 | ThreadPool threadPool() default @ThreadPool; 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/client/MultiPulsarClient.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.client; 2 | 3 | import io.github.howinfun.ececption.PulsarAutoConfigException; 4 | import io.github.howinfun.properties.MultiPulsarProperties; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.TimeUnit; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.pulsar.client.api.PulsarClient; 10 | import org.apache.pulsar.client.api.PulsarClientException; 11 | import org.apache.pulsar.shade.org.apache.commons.lang.StringUtils; 12 | import org.springframework.beans.factory.DisposableBean; 13 | 14 | /** 15 | * 多数据源Pulsar客户端 16 | * @author winfun 17 | * @date 2021/9/1 9:17 上午 18 | **/ 19 | @Slf4j 20 | public class MultiPulsarClient extends HashMap implements DisposableBean { 21 | 22 | public MultiPulsarClient(MultiPulsarProperties multiPulsarProperties){ 23 | Map serviceUrlMap = multiPulsarProperties.getServiceUrl(); 24 | if (null != serviceUrlMap && !serviceUrlMap.isEmpty()){ 25 | for (Entry entry : serviceUrlMap.entrySet()) { 26 | String sourceName = entry.getKey(); 27 | String serviceUrl = entry.getValue(); 28 | if (StringUtils.isNotBlank(serviceUrl)){ 29 | try { 30 | PulsarClient client = PulsarClient.builder().serviceUrl(serviceUrl) 31 | .enableTcpNoDelay(multiPulsarProperties.getEnableTcpNoDelayBySourceName(sourceName)) 32 | .operationTimeout(multiPulsarProperties.getOperationTimeoutBySourceName(sourceName), TimeUnit.SECONDS) 33 | .listenerThreads(multiPulsarProperties.getListenerThreadsBySourceName(sourceName)) 34 | .ioThreads(multiPulsarProperties.getIoThreadsBySourceName(sourceName)) 35 | .build(); 36 | log.info("[Pulsar] Client实例化成功, sourceName is {}, serviceUrl is {}",sourceName,serviceUrl); 37 | this.put(sourceName,client); 38 | } catch (PulsarClientException e) { 39 | log.error("[Pulsar] Client实例化失败!"); 40 | throw new PulsarAutoConfigException("[Pulsar] Client实例化失败!", e); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | @Override 48 | public void destroy() throws Exception { 49 | this.values().forEach(pulsarClient -> { 50 | try { 51 | pulsarClient.close(); 52 | log.info("[Pulsar] 客户端关闭成功"); 53 | } catch (PulsarClientException e) { 54 | log.error("[Pulsar] 客户端关闭失败",e); 55 | } 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/listener/BaseMessageListener.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.listener; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.Executor; 5 | import java.util.concurrent.LinkedBlockingDeque; 6 | import java.util.concurrent.ThreadPoolExecutor; 7 | import java.util.concurrent.TimeUnit; 8 | import jodd.util.concurrent.ThreadFactoryBuilder; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.pulsar.client.api.Consumer; 11 | import org.apache.pulsar.client.api.Message; 12 | import org.apache.pulsar.client.api.MessageListener; 13 | 14 | /** 15 | * 基础MessageListener 16 | * 配合@PulsarListener注解使用,集成BaseMessageListener后,需主动注入IOC容器中 17 | * 自动配置会根据@PulsarListener的信息进行consumer的初始化 18 | * 支持线程池异步消费消息 19 | * @author winfun 20 | **/ 21 | @Slf4j 22 | public abstract class BaseMessageListener implements MessageListener { 23 | 24 | private Executor executor; 25 | 26 | /*** 27 | * 初始化Consumer线程池 28 | * @author winfun 29 | * @param corePoolSize 核心线程数 30 | * @param maximumPoolSize 最大核心线程数 31 | * @param keepAliveTime 线程保活时长,单位分钟 32 | * @param maxQueueLength 最大等待队列长度 33 | * @return {@link Void } 34 | **/ 35 | public void initThreadPool(Integer corePoolSize,Integer maximumPoolSize,Integer keepAliveTime,Integer maxQueueLength,String threadPoolName){ 36 | if (Objects.isNull(this.executor) && Boolean.TRUE.equals(this.enableAsync())){ 37 | this.executor = new ThreadPoolExecutor( 38 | corePoolSize, 39 | maximumPoolSize, 40 | keepAliveTime, 41 | TimeUnit.MINUTES, 42 | new LinkedBlockingDeque<>(maxQueueLength), 43 | new ThreadFactoryBuilder().setNameFormat(threadPoolName+"-%d").get(), 44 | // 使用CallerRunsPolicy拒绝策略,让当前线程执行,避免消息丢失 45 | new ThreadPoolExecutor.CallerRunsPolicy() 46 | ); 47 | log.info("[Pulsar] Consumer消费线程池初始化成功!"); 48 | } 49 | } 50 | 51 | @Override 52 | public void received(Consumer consumer, Message msg) { 53 | /** 54 | * 线程池异步执行 55 | */ 56 | if (Objects.nonNull(this.executor) && Boolean.TRUE.equals(this.enableAsync())) { 57 | this.executor.execute(() -> this.doReceived(consumer, msg)); 58 | }else { 59 | /** 60 | * 当前线程同步执行 61 | */ 62 | this.doReceived(consumer,msg); 63 | } 64 | } 65 | 66 | /** 67 | * 消费消息 68 | * 自定义监听器实现方法 69 | * 消息如何响应由开发者决定: 70 | * Consumer#acknowledge 71 | * Consumer#reconsumeLater 72 | * Consumer#negativeAcknowledge 73 | * @param consumer 消费者 74 | * @param msg 消息 75 | */ 76 | protected abstract void doReceived(Consumer consumer, Message msg); 77 | 78 | /*** 79 | * 是否开启异步消费,默认开启 80 | * @return {@link Boolean } 81 | **/ 82 | public Boolean enableAsync(){ 83 | return Boolean.TRUE; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/properties/MultiPulsarProperties.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.properties; 2 | 3 | import java.util.Map; 4 | import lombok.Data; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.CollectionUtils; 9 | 10 | /** 11 | * Pulsar配置类 12 | * 支持多数据源注入:key-数据源名称、value-对应配置值 13 | * @author winfun 14 | **/ 15 | @Data 16 | @Component 17 | @ConfigurationProperties(prefix = "pulsar") 18 | @ConditionalOnExpression("!'${pulsar.serviceUrl}'.isEmpty()") 19 | public class MultiPulsarProperties { 20 | 21 | /** 22 | * 默认数据源名称:default 23 | */ 24 | public static final String DEFAULT_SOURCE_NAME = "default"; 25 | /** 26 | * pulsar服务地址 27 | */ 28 | private Map serviceUrl; 29 | /** 30 | * 租户 31 | */ 32 | private Map tenant; 33 | /** 34 | * 命名空间 35 | */ 36 | private Map namespace; 37 | /** 38 | * 是否开启TCP不延迟 39 | */ 40 | private Map enableTcpNoDelay; 41 | /** 42 | * 操作超时,单位毫秒 43 | */ 44 | private Map operationTimeout; 45 | /** 46 | * 消费者监听线程数 47 | */ 48 | private Map listenerThreads; 49 | /** 50 | * IO线程数 51 | */ 52 | private Map ioThreads; 53 | 54 | /** 55 | * 是否开启TCP不延迟 56 | */ 57 | private Boolean defaultEnableTcpNoDelay=true; 58 | /** 59 | * 操作超时,单位秒 60 | */ 61 | private Integer defaultOperationTimeout=30; 62 | /** 63 | * 消费者监听线程数 64 | */ 65 | private Integer defaultListenerThreads=1; 66 | /** 67 | * IO线程数 68 | */ 69 | private Integer defaultIoThreads=1; 70 | 71 | /** 72 | * 根据数据源名称获取 pulsar服务地址 73 | * @param sourceName 数据源名称 74 | * @return pulsar服务地址 75 | */ 76 | public String getServiceUrlBySourceName(String sourceName){ 77 | if (CollectionUtils.isEmpty(this.serviceUrl)){ 78 | return null; 79 | } 80 | return this.serviceUrl.getOrDefault(sourceName,""); 81 | } 82 | 83 | /** 84 | * 根据数据源名称获取 租户 85 | * @param sourceName 数据源名称 86 | * @return 租户 87 | */ 88 | public String getTenantBySourceName(String sourceName){ 89 | if (CollectionUtils.isEmpty(this.tenant)){ 90 | return null; 91 | } 92 | return this.tenant.getOrDefault(sourceName,""); 93 | } 94 | 95 | /** 96 | * 根据数据源名称获取 命名空间 97 | * @param sourceName 数据源名称 98 | * @return 命名空间 99 | */ 100 | public String getNamespaceBySourceName(String sourceName){ 101 | if (CollectionUtils.isEmpty(this.namespace)){ 102 | return null; 103 | } 104 | return this.namespace.getOrDefault(sourceName,""); 105 | } 106 | 107 | /** 108 | * 根据数据源名称获取 enableTcpNoDelay 开关 109 | * @param sourceName 数据源名称 110 | * @return 开关 111 | */ 112 | public Boolean getEnableTcpNoDelayBySourceName(String sourceName){ 113 | if (CollectionUtils.isEmpty(this.enableTcpNoDelay)){ 114 | return this.defaultEnableTcpNoDelay; 115 | } 116 | return this.enableTcpNoDelay.getOrDefault(sourceName,this.defaultEnableTcpNoDelay); 117 | } 118 | 119 | /** 120 | * 根据数据源名称获取 操作超时时长 121 | * @param sourceName 数据源名称 122 | * @return 超时时长 123 | */ 124 | public Integer getOperationTimeoutBySourceName(String sourceName){ 125 | if (CollectionUtils.isEmpty(this.operationTimeout)){ 126 | return this.defaultOperationTimeout; 127 | } 128 | return this.operationTimeout.getOrDefault(sourceName,this.defaultOperationTimeout); 129 | } 130 | 131 | /** 132 | * 根据数据源名称获取 消费者监听线程数 133 | * @param sourceName 数据源名称 134 | * @return 消费者监听线程数 135 | */ 136 | public Integer getListenerThreadsBySourceName(String sourceName){ 137 | if (CollectionUtils.isEmpty(this.listenerThreads)){ 138 | return this.defaultListenerThreads; 139 | } 140 | return this.listenerThreads.getOrDefault(sourceName,this.defaultListenerThreads); 141 | } 142 | 143 | /** 144 | * 根据数据源名称获取 IO线程数 145 | * @param sourceName 数据源名称 146 | * @return IO线程数 147 | */ 148 | public Integer getIoThreadsBySourceName(String sourceName){ 149 | if (CollectionUtils.isEmpty(this.ioThreads)){ 150 | return this.defaultIoThreads; 151 | } 152 | return this.ioThreads.getOrDefault(sourceName,this.defaultIoThreads); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | 4 | # Compiled class file 5 | *.class 6 | 7 | # Log file 8 | *.log 9 | 10 | # BlueJ files 11 | *.ctxt 12 | 13 | # Mobile Tools for Java (J2ME) 14 | .mtj.tmp/ 15 | 16 | # Package Files # 17 | *.jar 18 | *.war 19 | *.nar 20 | *.ear 21 | *.zip 22 | *.tar.gz 23 | *.rar 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | 28 | 29 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 30 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 31 | 32 | # User-specific stuff 33 | *.iml 34 | 35 | # CMake 36 | cmake-build-*/ 37 | 38 | # Mongo Explorer plugin 39 | .idea/**/mongoSettings.xml 40 | 41 | # File-based project format 42 | *.iws 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # Crashlytics plugin (for Android Studio and IntelliJ) 57 | com_crashlytics_export_strings.xml 58 | crashlytics.properties 59 | crashlytics-build.properties 60 | fabric.properties 61 | 62 | # Editor-based Rest Client 63 | .idea/httpRequests 64 | 65 | .metadata 66 | bin/ 67 | tmp/ 68 | *.tmp 69 | *.bak 70 | *.swp 71 | *~.nib 72 | local.properties 73 | .settings/ 74 | .loadpath 75 | .recommenders 76 | 77 | # External tool builders 78 | .externalToolBuilders/ 79 | 80 | # Locally stored "Eclipse launch configurations" 81 | *.launch 82 | 83 | # PyDev specific (Python IDE for Eclipse) 84 | *.pydevproject 85 | 86 | # CDT-specific (C/C++ Development Tooling) 87 | .cproject 88 | 89 | # CDT- autotools 90 | .autotools 91 | 92 | # Java annotation processor (APT) 93 | .factorypath 94 | 95 | # PDT-specific (PHP Development Tools) 96 | .buildpath 97 | 98 | # sbteclipse plugin 99 | .target 100 | 101 | # Tern plugin 102 | .tern-project 103 | 104 | # TeXlipse plugin 105 | .texlipse 106 | 107 | # STS (Spring Tool Suite) 108 | .springBeans 109 | 110 | # Code Recommenders 111 | .recommenders/ 112 | 113 | # Annotation Processing 114 | .apt_generated/ 115 | 116 | # Scala IDE specific (Scala & Java development for Eclipse) 117 | .cache-main 118 | .scala_dependencies 119 | .worksheet 120 | 121 | .vscode/* 122 | .project 123 | */.project 124 | */.classpath 125 | !.vscode/settings.json 126 | !.vscode/tasks.json 127 | !.vscode/launch.json 128 | !.vscode/extensions.json 129 | 130 | 131 | # General 132 | .DS_Store 133 | .AppleDouble 134 | .LSOverride 135 | 136 | # Icon must end with two \r 137 | Icon 138 | 139 | # Thumbnails 140 | ._* 141 | 142 | # Files that might appear in the root of a volume 143 | .DocumentRevisions-V100 144 | .fseventsd 145 | .Spotlight-V100 146 | .TemporaryItems 147 | .Trashes 148 | .VolumeIcon.icns 149 | .com.apple.timemachine.donotpresent 150 | 151 | # Directories potentially created on remote AFP share 152 | .AppleDB 153 | .AppleDesktop 154 | Network Trash Folder 155 | Temporary Items 156 | .apdisk 157 | 158 | # Windows thumbnail cache files 159 | Thumbs.db 160 | ehthumbs.db 161 | ehthumbs_vista.db 162 | 163 | # Dump file 164 | *.stackdump 165 | 166 | # Folder config file 167 | [Dd]esktop.ini 168 | 169 | # Recycle Bin used on file shares 170 | $RECYCLE.BIN/ 171 | 172 | # Windows Installer files 173 | *.cab 174 | *.msi 175 | *.msix 176 | *.msm 177 | *.msp 178 | 179 | # Windows shortcuts 180 | *.lnk 181 | 182 | *~ 183 | 184 | # temporary files which can be created if a process still has a handle open of a deleted file 185 | .fuse_hidden* 186 | 187 | # KDE directory preferences 188 | .directory 189 | 190 | # Linux trash folder which might appear on any partition or disk 191 | .Trash-* 192 | 193 | # .nfs files are created when an open file is removed but is still being accessed 194 | .nfs* 195 | 196 | 197 | target/ 198 | pom.xml.tag 199 | pom.xml.releaseBackup 200 | pom.xml.versionsBackup 201 | pom.xml.next 202 | release.properties 203 | dependency-reduced-pom.xml 204 | buildNumber.properties 205 | .mvn/timing.properties 206 | .mvn/wrapper/maven-wrapper.jar 207 | 208 | .gradle 209 | /build/ 210 | 211 | # Ignore Gradle GUI config 212 | gradle-app.setting 213 | 214 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 215 | !gradle-wrapper.jar 216 | 217 | # Cache of project 218 | .gradletasknamecache 219 | 220 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 221 | # gradle/wrapper/gradle-wrapper.properties 222 | 223 | # Project-level settings 224 | /.tgitconfig 225 | 226 | .svn/ 227 | # generator/src/main/java/* 228 | 229 | rebel.xml 230 | 231 | .mvn/ 232 | 233 | # .idea folder 234 | .idea/ 235 | .idea/* 236 | !.idea/misc.xml 237 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/template/PulsarTemplate.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.template; 2 | 3 | import io.github.howinfun.client.MultiPulsarClient; 4 | import io.github.howinfun.ececption.PulsarBusinessException; 5 | import io.github.howinfun.properties.MultiPulsarProperties; 6 | import io.github.howinfun.utils.TopicUtil; 7 | import java.util.Objects; 8 | import java.util.concurrent.CompletableFuture; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.ExecutionException; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.apache.pulsar.client.api.MessageId; 13 | import org.apache.pulsar.client.api.Producer; 14 | import org.apache.pulsar.client.api.PulsarClient; 15 | import org.apache.pulsar.client.api.PulsarClientException; 16 | import org.apache.pulsar.client.api.Schema; 17 | import org.apache.pulsar.shade.org.apache.commons.lang.StringUtils; 18 | 19 | /** 20 | * Pulsar Producer Template 21 | * @author winfun 22 | **/ 23 | @Slf4j 24 | public class PulsarTemplate { 25 | 26 | /** 27 | * producer 缓存 28 | * key:topic,value:producer 29 | */ 30 | private final ConcurrentHashMap> producerCaches = new ConcurrentHashMap<>(64); 31 | /** 32 | * 多数据源Pulsar客户端 33 | */ 34 | private final MultiPulsarClient multiPulsarClient; 35 | /** 36 | * 多数据源Pulsar自定义配置 37 | */ 38 | private final MultiPulsarProperties multiPulsarProperties; 39 | 40 | public PulsarTemplate(MultiPulsarClient multiPulsarClient, MultiPulsarProperties multiPulsarProperties){ 41 | this.multiPulsarClient = multiPulsarClient; 42 | this.multiPulsarProperties = multiPulsarProperties; 43 | } 44 | 45 | /** 46 | * 创建Builder 47 | * @return Builder 48 | */ 49 | public Builder createBuilder(){ 50 | return new Builder(); 51 | } 52 | 53 | /** 54 | * Builder模式 55 | * 开发者可自行指定租户/命名空间,如果不指定,则使用配置文件 56 | */ 57 | public class Builder { 58 | 59 | /** 60 | * 数据源名称 61 | * 默认值:{@link MultiPulsarProperties#DEFAULT_SOURCE_NAME} 62 | */ 63 | private String sourceName; 64 | /** 65 | * 是否持久化 66 | */ 67 | private Boolean persistent; 68 | /** 69 | * 租户 70 | */ 71 | private String tenant; 72 | /** 73 | * 命名空间 74 | */ 75 | private String namespace; 76 | /** 77 | * 主题 78 | */ 79 | private String topic; 80 | 81 | public Builder sourceName(String sourceName){ 82 | this.sourceName = sourceName; 83 | return this; 84 | } 85 | 86 | public Builder persistent(Boolean persistent){ 87 | this.persistent = persistent; 88 | return this; 89 | } 90 | 91 | public Builder tenant(String tenant){ 92 | this.tenant = tenant; 93 | return this; 94 | } 95 | 96 | public Builder namespace(String namespace){ 97 | this.namespace = namespace; 98 | return this; 99 | } 100 | 101 | public Builder topic(String topic){ 102 | this.topic = topic; 103 | return this; 104 | } 105 | 106 | /** 107 | * 同步发送消息 108 | * @param msg 消息 109 | * @return 消息ID 110 | */ 111 | public MessageId send(String msg) throws Exception{ 112 | try { 113 | MessageId messageId = this.sendAsync(msg).get(); 114 | log.info("[Pulsar] Producer同步发送消息成功,msg is {}",msg); 115 | return messageId; 116 | } catch (InterruptedException | ExecutionException e) { 117 | log.error("[Pulsar] Producer同步发送消息失败,msg is {}",msg); 118 | throw e; 119 | } 120 | } 121 | 122 | /** 123 | * 异步发送消息 124 | * @param msg 消息 125 | * @return CompletableFuture 126 | */ 127 | public CompletableFuture sendAsync(String msg) throws PulsarClientException{ 128 | 129 | String finalTopic = this.generateTopic(); 130 | String sourceName = StringUtils.isNotBlank(this.sourceName) ? this.sourceName : MultiPulsarProperties.DEFAULT_SOURCE_NAME; 131 | try { 132 | Producer producer = PulsarTemplate.this.producerCaches.getOrDefault(finalTopic,null); 133 | if (Objects.isNull(producer)){ 134 | PulsarClient client = PulsarTemplate.this.multiPulsarClient.getOrDefault(sourceName,null); 135 | if (Objects.isNull(client)){ 136 | log.error("[Pulsar] 数据源对应PulsarClient不存在,sourceName is {}",sourceName); 137 | throw new PulsarBusinessException("[Pulsar] 数据源对应PulsarClient不存在!"); 138 | } 139 | producer = client.newProducer(Schema.STRING).topic(finalTopic).create(); 140 | PulsarTemplate.this.producerCaches.put(finalTopic,producer); 141 | log.info("[Pulsar] Producer实例化成功,sourceName is {}, topic is {}",sourceName,finalTopic); 142 | } 143 | return producer.sendAsync(msg); 144 | } catch (Exception e) { 145 | log.error("[Pulsar] Producer实例化失败,topic is {}",finalTopic); 146 | throw e; 147 | } 148 | } 149 | 150 | /** 151 | * 拼接topic 152 | * @return 完整topic路径 153 | */ 154 | private String generateTopic(){ 155 | if (StringUtils.isBlank(this.topic)){ 156 | log.error("[Pulsar] Topic 为空,无法发送消息, topic is {}",this.topic); 157 | throw new PulsarBusinessException("Topic不能为空"); 158 | } 159 | String finalTenant = StringUtils.isNotBlank(this.tenant)?this.tenant:PulsarTemplate.this.multiPulsarProperties.getTenantBySourceName(this.sourceName); 160 | String finalNamespace = StringUtils.isNotBlank(this.namespace)?this.namespace:PulsarTemplate.this.multiPulsarProperties.getNamespaceBySourceName(this.sourceName); 161 | if (StringUtils.isBlank(finalTenant) || StringUtils.isBlank(finalNamespace)){ 162 | log.error("[Pulsar] 租户||命名空间为空,无法创建发送消息, tenant is {}, namespace is {}",finalTenant,finalNamespace); 163 | throw new PulsarBusinessException("租户||命名空间为空,无法发送消息"); 164 | } 165 | Boolean finalPersistent = Objects.nonNull(this.persistent)?this.persistent:Boolean.TRUE; 166 | return TopicUtil.generateTopic(finalPersistent, finalTenant, finalNamespace, this.topic); 167 | } 168 | 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.howinfun 8 | winfun-pulsar-spring-boot-starter 9 | 1.2.0 10 | winfun-pulsar-spring-boot-starter 11 | 统一封装pulsar 12 | https://github.com/Howinfun/winfun-pulsar-spring-boot-starter 13 | 14 | 15 | 1.8 16 | UTF-8 17 | 2.1.6.RELEASE 18 | 1.18.10 19 | 5.4.2 20 | 2.6.1 21 | 1.7.30 22 | 6.1.7.Final 23 | 5.0.13 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-configuration-processor 31 | ${spring-boot.version} 32 | true 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-autoconfigure 38 | ${spring-boot.version} 39 | 40 | 41 | org.projectlombok 42 | lombok 43 | ${lombok.version} 44 | 45 | 46 | 47 | org.apache.pulsar 48 | pulsar-client 49 | ${pulsar-client.version} 50 | 51 | 52 | cn.hutool 53 | hutool-all 54 | ${hutool.version} 55 | 56 | 57 | org.jodd 58 | jodd-core 59 | ${jodd-core.version} 60 | 61 | 62 | 63 | org.slf4j 64 | slf4j-api 65 | ${sl4j-api.version} 66 | 67 | 68 | 69 | org.hibernate.validator 70 | hibernate-validator 71 | ${hibernate-validator.version} 72 | 73 | 74 | 75 | 76 | 77 | 78 | MIT License 79 | http://www.opensource.org/licenses/mit-license.php 80 | 81 | 82 | 83 | 84 | 85 | Howinfun 86 | 876237770@qq.com 87 | https://github.com/Howinfun 88 | 89 | 90 | 91 | 92 | https://github.com/Howinfun/winfun-pulsar-spring-boot-starter 93 | https://github.com/Howinfun/winfun-pulsar-spring-boot-starter.git 94 | https://github.com/Howinfun 95 | 96 | 97 | 98 | 99 | ossrh 100 | https://s01.oss.sonatype.org/content/repositories/snapshots/ 101 | 102 | 103 | ossrh 104 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-compiler-plugin 113 | 3.8.1 114 | 115 | 1.8 116 | 1.8 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-source-plugin 122 | 3.0.1 123 | 124 | 125 | attach-sources 126 | 127 | jar-no-fork 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-javadoc-plugin 135 | 2.9.1 136 | 137 | 138 | attach-javadocs 139 | 140 | jar 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-gpg-plugin 148 | 1.1 149 | 150 | 151 | sign-artifacts 152 | verify 153 | 154 | sign 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | disable-javadoc-doclint 164 | 165 | [1.8,) 166 | 167 | 168 | -Xdoclint:none 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # winfun-pulsar-spring-boot-starter 2 | 3 | 4 | # 里程碑 5 | 6 | 7 | 版本 | 功能点 | 作者 | 完成 8 | ---|---|---|--- 9 | 1.0.0 | 支持PulsarTemplate发送消息&支持自定义注解实例化Consumer监听消息 | howinfun | ✅ 10 | 1.1.0 | 支持动态开启/关闭Consumer消费线程池、支持自定义配置Consuemr消费线程池参数 | howinfun | ✅ 11 | 1.2.0 | 支持多Pulsar数据源 | howinfun | ✅ 12 | 1.2.0 | 支持Spring容器停止时,释放Pulsar所有相关资源 | howinfun | ✅ 13 | 14 | # 一、背景 15 | 16 | Pulsar 作为新生代云原生消息队列,越来越受到开发者的热爱;而我们现在基本上的项目都是基于 SpringBoot 上开发的,但是我们可以发现,至今都没有比较大众和成熟的关于 Pulsar 的 Starter,所以我们需要自己整一个,从而避免常规使用 Pulsar API 时产生大量的重复代码。 17 | 18 | # 二、设计思路 19 | 20 | 由于是第一版的设计,所以我们是从简单开始,不会一开始就设计得很复杂,尽量保留 Pulsar API 原生的功能。 21 | 22 | ## 2.1、PulsarClient 23 | 24 | 我们都知道,不管是 Producer 还是 Consumer,都是由 PulsarClient 创建的。 25 | 26 | 当然了,PulsarClient 可以根据业务需要自定义很多参数,但是第一版的设计只会支持比较常用的参数。 27 | 28 | 我们这个组件支持下面功能点: 29 | - 支持 PulsarClient 参数配置外部化,参数可配置在 applicatin.properties 中。 30 | - 支持 applicatin.properties 提供配置提示信息。 31 | - 读取外部配置文件,根据参数实例化 PulsarClient,并注入到 IOC 容器中。 32 | 33 | 34 | ## 2.2、Producer 35 | 36 | Producer是发送消息的组件。 37 | 38 | - 这里我们提供一个模版类,可以根据需求创建对应的 Producer 实例。 39 | - 支持将 Topic<->Producer 关系缓存起来,避免重复创建 Producer 实例。 40 | - 支持同步/异步发送消息。 41 | 42 | ## 2.3、Consumer 43 | 44 | Consumer是消费消息的组件。 45 | 46 | - 这里我们提供一个抽象类,开发者只需要集成此实现类并实现 doReceive 方法即可,即消费消息的逻辑方法。 47 | - 接着还提供一个自定义注解,自定义注解支持自定义 Consmuer 配置,例如Topic、Tenant、Namespace等。 48 | - 实现类加入上述自定义注解后,组件将会自动识别并且生成对应的 Consumer 实例。 49 | - 支持同步/线程池异步消费。 50 | 51 | # 三、使用例子 52 | 53 | 54 | ## 3.1、winfun-pulsar-spring-boot-starter:1.1.0 版本 55 | 56 | 第一个版本,不支持多数据源。 57 | 58 | ### 3.1.1、引入依赖 59 | 60 | ```xml 61 | 62 | io.github.howinfun 63 | winfun-pulsar-spring-boot-starter 64 | 1.1.0 65 | 66 | ``` 67 | 68 | ### 3.1.2、加入配置 69 | 70 | ```properties 71 | pulsar.service-url=pulsar://127.0.0.1:6650 72 | pulsar.tenant=winfun 73 | pulsar.namespace=study 74 | pulsar.operation-timeout=30 75 | pulsar.io-threads=10 76 | pulsar.listener-threads=10 77 | ``` 78 | 79 | ### 3.1.3、发送消息 80 | 81 | ```java 82 | /** 83 | * 发送消息 84 | * @author: winfun 85 | **/ 86 | @RestController 87 | @RequestMapping("msg") 88 | public class MessageController { 89 | 90 | @Autowired 91 | private PulsarTemplate pulsarTemplate; 92 | @Autowired 93 | private PulsarProperties pulsarProperties; 94 | 95 | /*** 96 | * 往指定topic发送消息 97 | * @author winfun 98 | * @param topic topic 99 | * @param msg msg 100 | * @return {@link String } 101 | **/ 102 | @GetMapping("/{topic}/{msg}") 103 | public String send(@PathVariable("topic") String topic,@PathVariable("msg") String msg) throws Exception { 104 | this.pulsarTemplate.createBuilder().persistent(Boolean.TRUE) 105 | .tenant(this.pulsarProperties.getTenant()) 106 | .namespace(this.pulsarProperties.getNamespace()) 107 | .topic(topic) 108 | .send(msg); 109 | return "success"; 110 | } 111 | } 112 | ``` 113 | 114 | ### 3.1.4、消费消息 115 | 116 | ```java 117 | /** 118 | * @author: winfun 119 | **/ 120 | @Slf4j 121 | @PulsarListener(topics = {"test-topic2"}, 122 | threadPool = @ThreadPool( 123 | coreThreads = 2, 124 | maxCoreThreads = 3, 125 | threadPoolName = "test-thread-pool")) 126 | public class ConsumerListener extends BaseMessageListener { 127 | 128 | /** 129 | * 消费消息 130 | * @param consumer 消费者 131 | * @param msg 消息 132 | */ 133 | @Override 134 | protected void doReceived(Consumer consumer, Message msg) { 135 | log.info("成功消费消息:{}",msg.getValue()); 136 | try { 137 | consumer.acknowledge(msg); 138 | } catch (PulsarClientException e) { 139 | e.printStackTrace(); 140 | } 141 | } 142 | 143 | /*** 144 | * 是否开启异步消费 145 | * @return {@link Boolean } 146 | **/ 147 | @Override 148 | public Boolean enableAsync() { 149 | return Boolean.TRUE; 150 | } 151 | } 152 | ``` 153 | 154 | ## 3.2、winfun-pulsar-spring-boot-starter:1.2.0 版本 155 | 156 | 第二个版本,支持多数据源 157 | 158 | ### 3.2.1、引入依赖 159 | 160 | ```xml 161 | 162 | io.github.howinfun 163 | winfun-pulsar-spring-boot-starter 164 | 1.2.0 165 | 166 | ``` 167 | 168 | ### 3.2.2、加入配置 169 | 170 | ```properties 171 | pulsar.serviceUrl.default=pulsar://127.0.0.1:6650 172 | pulsar.tenant.default=winfun 173 | pulsar.namespace.default=study 174 | pulsar.enableTcpNoDelay.default=true 175 | pulsar.operationTimeout.default=3 176 | pulsar.ioThreads.default=5 177 | pulsar.listenerThreads.default=5 178 | 179 | 180 | # 如果有第二个数据源,可这样配置 181 | pulsar.serviceUrl.second=pulsar://127.0.0.1:6651 182 | pulsar.tenant.second=winfun 183 | pulsar.namespace.second=study 184 | ..... 185 | ``` 186 | 187 | ### 3.2.3、发送消息 188 | 189 | 和上面的第一个版本不同,我们不再需要指定tenant和namespace、只需要指定sourceName即可;如果sourceName也不指定,默认使用"default"。 190 | ```java 191 | /** 192 | * 发送消息 193 | * @author: winfun 194 | **/ 195 | @RestController 196 | @RequestMapping("msg") 197 | public class MessageController { 198 | 199 | @Autowired 200 | private PulsarTemplate pulsarTemplate; 201 | 202 | /*** 203 | * 往指定topic发送消息 204 | * @author winfun 205 | * @param topic topic 206 | * @param msg msg 207 | * @return {@link String } 208 | **/ 209 | @GetMapping("/{sourceName}/{topic}/{msg}") 210 | public String send(@PathVariable("sourceName") String sourceName,@PathVariable("topic") String topic,@PathVariable("msg") String msg) throws Exception { 211 | this.pulsarTemplate.createBuilder().persistent(Boolean.TRUE) 212 | .sourceName(sourceName) 213 | .topic(topic) 214 | .send(msg); 215 | return "success"; 216 | } 217 | } 218 | ``` 219 | 220 | ### 3.2.4、消费消息 221 | 222 | 消费者在第二版中,基本和第一版一致,只是多出了一个sourceName属性,可自行指定使用哪个数据源,如果不指定,默认使用"default"。 223 | ```java 224 | /** 225 | * 消费消息 226 | * @author: winfun 227 | **/ 228 | @Slf4j 229 | @PulsarListener(sourceName = "default", 230 | topics = {"test-topic2"}, 231 | threadPool = @ThreadPool( 232 | coreThreads = 2, 233 | maxCoreThreads = 3, 234 | threadPoolName = "test-thread-pool")) 235 | public class ConsumerListener extends BaseMessageListener { 236 | 237 | /** 238 | * 消费消息 239 | * @param consumer 消费者 240 | * @param msg 消息 241 | */ 242 | @Override 243 | protected void doReceived(Consumer consumer, Message msg) { 244 | log.info("成功消费消息:{}",msg.getValue()); 245 | try { 246 | consumer.acknowledge(msg); 247 | } catch (PulsarClientException e) { 248 | e.printStackTrace(); 249 | } 250 | } 251 | 252 | /*** 253 | * 是否开启异步消费 254 | * @return {@link Boolean } 255 | **/ 256 | @Override 257 | public Boolean enableAsync() { 258 | return Boolean.TRUE; 259 | } 260 | } 261 | ``` 262 | 263 | # 四、源码 264 | 265 | 源码就不放在这里分析了,大家可到[Github](https://github.com/Howinfun/winfun-pulsar-spring-boot-starter)上看看,如果有什么代码上面的建议或意见,欢迎大家提MR。 266 | -------------------------------------------------------------------------------- /src/main/java/io/github/howinfun/configuration/PulsarConsumerAutoConfigure.java: -------------------------------------------------------------------------------- 1 | package io.github.howinfun.configuration; 2 | 3 | import cn.hutool.core.util.RandomUtil; 4 | import io.github.howinfun.client.MultiPulsarClient; 5 | import io.github.howinfun.ececption.PulsarAutoConfigException; 6 | import io.github.howinfun.listener.BaseMessageListener; 7 | import io.github.howinfun.listener.PulsarListener; 8 | import io.github.howinfun.listener.ThreadPool; 9 | import io.github.howinfun.properties.MultiPulsarProperties; 10 | import io.github.howinfun.utils.TopicUtil; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.concurrent.TimeUnit; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.apache.pulsar.client.api.Consumer; 17 | import org.apache.pulsar.client.api.ConsumerBuilder; 18 | import org.apache.pulsar.client.api.DeadLetterPolicy; 19 | import org.apache.pulsar.client.api.PulsarClient; 20 | import org.apache.pulsar.client.api.PulsarClientException; 21 | import org.apache.pulsar.client.api.Schema; 22 | import org.apache.pulsar.client.api.SubscriptionType; 23 | import org.apache.pulsar.shade.org.apache.commons.lang.StringUtils; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.boot.CommandLineRunner; 26 | import org.springframework.core.annotation.AnnotationUtils; 27 | import org.springframework.util.CollectionUtils; 28 | 29 | /** 30 | * 消费者自动初始化 31 | * @author winfun 32 | **/ 33 | @Slf4j 34 | public class PulsarConsumerAutoConfigure implements CommandLineRunner { 35 | 36 | /** 37 | * 自定义消费者监听器列表,可为空 38 | */ 39 | @Autowired(required = false) 40 | private List listeners; 41 | /** 42 | * 多数据源Pulsar客户端 43 | */ 44 | private final MultiPulsarClient multiPulsarClient; 45 | /** 46 | * 多数据源Pulsar自定义配置 47 | */ 48 | private final MultiPulsarProperties multiPulsarProperties; 49 | 50 | public PulsarConsumerAutoConfigure(MultiPulsarClient multiPulsarClient, MultiPulsarProperties multiPulsarProperties){ 51 | this.multiPulsarClient = multiPulsarClient; 52 | this.multiPulsarProperties = multiPulsarProperties; 53 | } 54 | 55 | /** 56 | * 注入消费者到IOC容器 57 | * @param args 58 | * @throws Exception 59 | */ 60 | @Override 61 | public void run(String... args) throws Exception { 62 | if (!CollectionUtils.isEmpty(this.listeners)){ 63 | for (BaseMessageListener baseMessageListener : this.listeners) { 64 | // 获取当前监听器的@PulsarListener信息 65 | PulsarListener pulsarListener = AnnotationUtils.findAnnotation(baseMessageListener.getClass(), PulsarListener.class); 66 | if (Objects.nonNull(pulsarListener)){ 67 | try { 68 | String sourceName = pulsarListener.sourceName(); 69 | PulsarClient client = this.multiPulsarClient.getOrDefault(sourceName, null); 70 | if (Objects.isNull(client)){ 71 | log.error("[Pulsar] 数据源对应PulsarClient不存在,sourceName is {}",sourceName); 72 | continue; 73 | } 74 | ConsumerBuilder consumerBuilder = client.newConsumer(Schema.STRING).receiverQueueSize(pulsarListener.receiverQueueSize()); 75 | if (pulsarListener.topics().length > 0){ 76 | /** 77 | * 初始化线程池 78 | */ 79 | if (Boolean.TRUE.equals(baseMessageListener.enableAsync())){ 80 | log.info("[Pulsar] 消费者开启异步消费,开始初始化消费线程池...."); 81 | ThreadPool threadPool = pulsarListener.threadPool(); 82 | baseMessageListener.initThreadPool(threadPool.coreThreads(), threadPool.maxCoreThreads(), threadPool.keepAliveTime(), threadPool.maxQueueLength(), threadPool.threadPoolName()); 83 | } 84 | List topics = new ArrayList<>(pulsarListener.topics().length); 85 | String tenant = StringUtils.isBlank(pulsarListener.tenant()) ? this.multiPulsarProperties.getTenantBySourceName(sourceName) : pulsarListener.tenant(); 86 | String namespace = StringUtils.isBlank(pulsarListener.namespace()) ? this.multiPulsarProperties.getNamespaceBySourceName(sourceName) : pulsarListener.namespace(); 87 | if (StringUtils.isBlank(tenant) || StringUtils.isBlank(namespace)){ 88 | log.error("[Pulsar] 消费者初始化失败,subscriptionName is {},sourceName is {},tenant is {},namespace is {}",pulsarListener.subscriptionName(),sourceName,tenant,namespace); 89 | continue; 90 | } 91 | Boolean persistent = pulsarListener.persistent(); 92 | /** 93 | * 处理topics 94 | */ 95 | for (String topic : pulsarListener.topics()) { 96 | topics.add(TopicUtil.generateTopic(persistent, tenant, namespace, topic)); 97 | } 98 | consumerBuilder.topics(topics); 99 | /** 100 | * 处理订阅名称 101 | */ 102 | String subscriptionName = StringUtils.isBlank(pulsarListener.subscriptionName())?"subscription_"+ RandomUtil.randomString(3):pulsarListener.subscriptionName(); 103 | consumerBuilder.subscriptionName(subscriptionName); 104 | consumerBuilder.ackTimeout(Long.parseLong(pulsarListener.ackTimeout()), TimeUnit.MILLISECONDS); 105 | consumerBuilder.subscriptionType(pulsarListener.subscriptionType()); 106 | /** 107 | * 处理死信策略 108 | */ 109 | if (Boolean.TRUE.equals(pulsarListener.enableRetry())){ 110 | DeadLetterPolicy deadLetterPolicy = DeadLetterPolicy.builder() 111 | .maxRedeliverCount(pulsarListener.maxRedeliverCount()) 112 | .build(); 113 | if (StringUtils.isNotBlank(pulsarListener.retryLetterTopic())){ 114 | deadLetterPolicy.setRetryLetterTopic(pulsarListener.retryLetterTopic()); 115 | } 116 | if (StringUtils.isNotBlank(pulsarListener.deadLetterTopic())){ 117 | deadLetterPolicy.setDeadLetterTopic(pulsarListener.deadLetterTopic()); 118 | } 119 | consumerBuilder.enableRetry(pulsarListener.enableRetry()).deadLetterPolicy(deadLetterPolicy); 120 | }else { 121 | if (StringUtils.isNotBlank(pulsarListener.deadLetterTopic())){ 122 | if (SubscriptionType.Exclusive.equals(pulsarListener.subscriptionType())){ 123 | throw new PulsarAutoConfigException("[Pulsar] 消费端仅支持在Shared/Key_Shared模式下单独使用死信队列"); 124 | } 125 | DeadLetterPolicy deadLetterPolicy = DeadLetterPolicy.builder() 126 | .maxRedeliverCount(pulsarListener.maxRedeliverCount()) 127 | .deadLetterTopic(pulsarListener.deadLetterTopic()) 128 | .build(); 129 | consumerBuilder.deadLetterPolicy(deadLetterPolicy); 130 | } 131 | } 132 | consumerBuilder.messageListener(baseMessageListener); 133 | Consumer consumer = consumerBuilder.subscribe(); 134 | log.info("[Pulsar] Consumer初始化完毕, sourceName is {}, topic is {},",sourceName,consumer.getTopic()); 135 | } 136 | } catch (PulsarClientException e) { 137 | throw new PulsarAutoConfigException("[Pulsar] consumer初始化异常",e); 138 | } 139 | } 140 | } 141 | }else { 142 | log.warn("[Pulsar] 未发现有Consumer"); 143 | } 144 | } 145 | } 146 | --------------------------------------------------------------------------------