RocketMq消息監聽程序中消除大量的if..else

RocketMq消息監聽程序消除大量的if..else

承接上一篇文章,如果消費端訂閱了多個topic和tag,則需要在消息監聽器類中添加if..else,根據topic和tag處理不同的業務邏輯,使得消息監聽類職責過重。

大概思路

消息監聽器類只負責監聽消息,獲取到消息後通過topic和tag路由到需要調用的服務,消費者只需要編寫對應的topic和tag的服務。

爲了監聽器類可以通過topic和tag路由到需要調用的服務,自定義一個消費者註解MQConsumeService(該註解包含topic和tag的定義),消費者實現類上添加該註解,然後就可以在監聽器類中通過反射獲取到實現類上面註解的topic和tag,和監聽到的topic和tag比較,如果相同則調用該服務。

定義MQConsumeService註解

package com.clouds.common.rocketmq.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.stereotype.Service;

import com.clouds.common.rocketmq.TopicEnum;


/**
 * 此註解用於標註消費者服務
 * .<br/>
 * 
 * Copyright: Copyright (c) 2017  zteits
 * 
 * @ClassName: MQConsumeService
 * @Description: 
 * @version: v1.0.0
 * @author: zhaowg
 * @date: 2018年3月2日 下午1:15:52
 * Modification History:
 * Date             Author          Version            Description
 *---------------------------------------------------------*
 * 2018年3月2日      zhaowg           v1.0.0               創建
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Service
public @interface MQConsumeService {
    /**
     * 消息主題
     */
     TopicEnum topic();

    /**
     * 消息標籤,如果是該主題下所有的標籤,使用“*”
     */
     String[] tags();


}

定義消費者返回消息Bean

package com.clouds.common.rocketmq.consumer.processor;

import java.io.Serializable;
/**
 * 消費結果
 * .<br/>
 * 
 * Copyright: Copyright (c) 2017  zteits
 * 
 * @ClassName: MQConsumeResult
 * @Description: 
 * @version: v1.0.0
 * @author: zhaowg
 * @date: 2018年3月1日 上午11:12:55
 * Modification History:
 * Date             Author          Version            Description
 *---------------------------------------------------------*
 * 2018年3月1日      zhaowg           v1.0.0               創建
 */
public class MQConsumeResult implements Serializable{

    private static final long serialVersionUID = 1L;
    /**
     * 是否處理成功
     */
    private boolean isSuccess;
    /**
     * 如果處理失敗,是否允許消息隊列繼續調用,直到處理成功,默認true
     */
    private boolean isReconsumeLater = true;
    /**
     * 是否需要記錄消費日誌,默認不記錄
     */
    private boolean isSaveConsumeLog = false;
    /**
     * 錯誤Code
     */
    private String errCode;
    /**
     * 錯誤消息
     */
    private String errMsg;
    /**
     * 錯誤堆棧
     */
    private Throwable e;
    
    //省略set和get方法
}

定義統一的消息處理接口

package com.clouds.common.rocketmq.consumer.processor;

import java.util.List;

import com.alibaba.rocketmq.common.message.MessageExt;

/**
 * 消息隊列-消息消費處理接口
 * .<br/>
 * 
 * Copyright: Copyright (c) 2017  zteits
 * 
 * @ClassName: MQMsgProcessorService
 * @Description: 
 * @version: v1.0.0
 * @author: zhaowg
 * @date: 2018年3月1日 上午9:57:57
 * Modification History:
 * Date             Author          Version            Description
 *---------------------------------------------------------*
 * 2018年3月1日      zhaowg           v1.0.0               創建
 */
public interface MQMsgProcessor {
    /**
     * 消息處理<br/>
     * 如果沒有return true ,consumer會重新消費該消息,直到return true<br/>
     * consumer可能重複消費該消息,請在業務端自己做是否重複調用處理,該接口設計爲冪等接口
     * @param topic 消息主題
     * @param tag 消息標籤
     * @param msgs 消息
     * @return
     * 2018年3月1日 zhaowg
     */
    MQConsumeResult handle(String topic, String tag, List<MessageExt> msgs);
}

定義抽象類實現上面的MQMsgProcessor接口

package com.clouds.common.rocketmq.consumer.processor;

import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.rocketmq.common.message.MessageConst;
import com.alibaba.rocketmq.common.message.MessageExt;
/**
 * 所有消息處理繼承該類
 * .<br/>
 * 
 * Copyright: Copyright (c) 2017  zteits
 * 
 * @ClassName: AbstractMQMsgProcessorService
 * @Description: 
 * @version: v1.0.0
 * @author: zhaowg
 * @date: 2018年3月1日 上午11:14:25
 * Modification History:
 * Date             Author          Version            Description
 *---------------------------------------------------------*
 * 2018年3月1日      zhaowg           v1.0.0               創建
 */
public abstract class AbstractMQMsgProcessor implements MQMsgProcessor{
    
    protected static final Logger logger = LoggerFactory.getLogger(AbstractMQMsgProcessor.class);
    
    @Override
    public MQConsumeResult handle(String topic, String tag, List<MessageExt> msgs) {
        MQConsumeResult mqConsumeResult = new MQConsumeResult();
        /**可以增加一些其他邏輯*/
        
        for (MessageExt messageExt : msgs) {
            //消費具體的消息,拋出鉤子供真正消費該消息的服務調用
            mqConsumeResult = this.consumeMessage(tag,messageExt.getKeys()==null?null:Arrays.asList(messageExt.getKeys().split(MessageConst.KEY_SEPARATOR)),messageExt);
        }
        
        /**可以增加一些其他邏輯*/
        return mqConsumeResult;
    }
    /**
     * 消息某條消息
     * @param tag 標籤
     * @param keys 消息關鍵字
     * @param messageExt
     * @return
     * 2018年3月1日 zhaowg
     */
    protected abstract MQConsumeResult consumeMessage(String tag,List<String> keys, MessageExt messageExt);

}

修改後的消費者監聽器類

package com.clouds.common.rocketmq.consumer.processor;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.clouds.common.rocketmq.annotation.MQConsumeService;
import com.clouds.common.rocketmq.constants.RocketMQErrorEnum;
import com.clouds.common.rocketmq.exception.AppException;
import com.clouds.common.rocketmq.exception.RocketMQException;
/**
 * 消費者消費消息路由
 * .<br/>
 * 
 * Copyright: Copyright (c) 2017  zteits
 * 
 * @ClassName: RocketMQMessageListenerConcurrentlyProcessor
 * @Description: 
 * @version: v1.0.0
 * @author: zhaowg
 * @date: 2018年2月28日 上午11:12:32
 * Modification History:
 * Date             Author          Version            Description
 *---------------------------------------------------------*
 * 2018年2月28日      zhaowg           v1.0.0               創建
 */
@Component
public class MQConsumeMsgListenerProcessor implements MessageListenerConcurrently{
    private static final Logger logger = LoggerFactory.getLogger(MQConsumeMsgListenerProcessor.class);
    @Autowired
    private Map<String,MQMsgProcessor> mqMsgProcessorServiceMap;
    /**
     *  默認msgs裏只有一條消息,可以通過設置consumeMessageBatchMaxSize參數來批量接收消息<br/>
     *  不要拋異常,如果沒有return CONSUME_SUCCESS ,consumer會重新消費該消息,直到return CONSUME_SUCCESS
     */
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        if(CollectionUtils.isEmpty(msgs)){
            logger.info("接受到的消息爲空,不處理,直接返回成功");
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        ConsumeConcurrentlyStatus concurrentlyStatus = ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        try{
            //根據Topic分組
            Map<String, List<MessageExt>> topicGroups = msgs.stream().collect(Collectors.groupingBy(MessageExt::getTopic));
            for (Entry<String, List<MessageExt>> topicEntry : topicGroups.entrySet()) {
                String topic = topicEntry.getKey();
                //根據tags分組
                Map<String, List<MessageExt>> tagGroups = topicEntry.getValue().stream().collect(Collectors.groupingBy(MessageExt::getTags));
                for (Entry<String, List<MessageExt>> tagEntry : tagGroups.entrySet()) {
                    String tag = tagEntry.getKey();
                    //消費某個主題下,tag的消息
                    this.consumeMsgForTag(topic,tag,tagEntry.getValue());
                }
            }
        }catch(Exception e){
            logger.error("處理消息失敗",e);
            concurrentlyStatus = ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        // 如果沒有return success ,consumer會重新消費該消息,直到return success
        return concurrentlyStatus;
    }
    /**
     * 根據topic 和 tags路由,查找消費消息服務
     * @param topic
     * @param tag
     * @param value
     * 2018年3月1日 zhaowg
     */
    private void consumeMsgForTag(String topic, String tag, List<MessageExt> value) {
        //根據topic 和  tag查詢具體的消費服務
        MQMsgProcessor imqMsgProcessor = selectConsumeService(topic, tag);
        
        try{
            if(imqMsgProcessor==null){
                logger.error(String.format("根據Topic:%s和Tag:%s 沒有找到對應的處理消息的服務",topic,tag));
                throw new RocketMQException(RocketMQErrorEnum.NOT_FOUND_CONSUMESERVICE);
            }
            logger.info(String.format("根據Topic:%s和Tag:%s 路由到的服務爲:%s,開始調用處理消息",topic,tag,imqMsgProcessor.getClass().getName()));
            //調用該類的方法,處理消息
            MQConsumeResult mqConsumeResult = imqMsgProcessor.handle(topic,tag,value);
            if(mqConsumeResult==null){
                throw new RocketMQException(RocketMQErrorEnum.HANDLE_RESULT_NULL);
            }
            if(mqConsumeResult.isSuccess()){
                logger.info("消息處理成功:"+JSON.toJSONString(mqConsumeResult));
            }else{
                throw new RocketMQException(RocketMQErrorEnum.CONSUME_FAIL,JSON.toJSONString(mqConsumeResult),false);
            }
            if(mqConsumeResult.isSaveConsumeLog()){
                logger.debug("開始記錄消費日誌");
                //TODO 記錄消費日誌
            }
        }catch(Exception e){
            if(e instanceof AppException){
                AppException mqe = (AppException)e;
                //TODO 記錄消費失敗日誌
                throw new AppException(mqe.getErrCode(),mqe.getErrMsg(),false); 
            }else{
                //TODO 記錄消費失敗日誌
                throw e;
            }
        }
    }
    /**
     * 根據topic和tag查詢對應的具體的消費服務
     * @param topic
     * @param tag
     * @return
     * 2018年3月3日 zhaowg
     */
    private MQMsgProcessor selectConsumeService(String topic, String tag) {
        MQMsgProcessor imqMsgProcessor = null;
        for (Entry<String, MQMsgProcessor> entry : mqMsgProcessorServiceMap.entrySet()) {
            //獲取service實現類上註解的topic和tags
            MQConsumeService consumeService = entry.getValue().getClass().getAnnotation(MQConsumeService.class);
            if(consumeService == null){
                logger.error("消費者服務:"+entry.getValue().getClass().getName()+"上沒有添加MQConsumeService註解");
                continue;
            }
            String annotationTopic = consumeService.topic().getCode();
            if(!annotationTopic.equals(topic)){
                continue;
            }
            String[] tagsArr = consumeService.tags();
            //"*"號表示訂閱該主題下所有的tag
            if(tagsArr[0].equals("*")){
                //獲取該實例
                imqMsgProcessor = entry.getValue();
                break;
            }
            boolean isContains = Arrays.asList(tagsArr).contains(tag);
            if(isContains){
                //獲取該實例
                imqMsgProcessor = entry.getValue();
                break;
            }
        }
        return imqMsgProcessor;
    }

}

至此,通過topic和tag路由服務已經寫完了,現在以新增訂閱一個DemoTopic主題爲例,編寫消息接受方。

新建消費者類,繼承AbstractMQMsgProcessor類

注意:該類必須繼承AbstractMQMsgProcessor,且添加MQConsumeService註解,用於定義該類是針對那個topic和tag的消費服務。新增訂閱主題,還需要在application.propertis中修改rocketmq.consumer.topics。

package com.clouds.common.demo;

import java.util.List;

import com.alibaba.rocketmq.common.message.MessageExt;
import com.clouds.common.rocketmq.TopicEnum;
import com.clouds.common.rocketmq.annotation.MQConsumeService;
import com.clouds.common.rocketmq.consumer.processor.AbstractMQMsgProcessor;
import com.clouds.common.rocketmq.consumer.processor.MQConsumeResult;

@MQConsumeService(topic=TopicEnum.DemoTopic,tags={"*"})
public class DemoNewTopicConsumerMsgProcessImpl extends AbstractMQMsgProcessor{

    @Override
    protected MQConsumeResult consumeMessage(String tag, List<String> keys, MessageExt messageExt) {
        String msg = new String(messageExt.getBody());
        logger.info("獲取到的消息爲:"+msg);
        //TODO 判斷該消息是否重複消費(RocketMQ不保證消息不重複,如果你的業務需要保證嚴格的不重複消息,需要你自己在業務端去重)
        
        //如果註解中tags數據中包含多個tag或者是全部的tag(*),則需要根據tag判斷是那個業務,
        //如果註解中tags爲具體的某個tag,則該服務就是單獨針對tag處理的
        if(tag.equals("某個tag")){
            //做某個操作
        }
        //TODO 獲取該消息重試次數
        int reconsume = messageExt.getReconsumeTimes();
        //根據消息重試次數判斷是否需要繼續消費
        if(reconsume ==3){//消息已經重試了3次,如果不需要再次消費,則返回成功
            
        }
        MQConsumeResult result = new MQConsumeResult();
        result.setSuccess(true);
        return result;
    }

}

rocketmq.consumer.topics=DemoTopic~*;

運行測試類觀察日誌


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

2018-03-03 16:54:25.079  INFO 12496 --- [           main] c.c.c.SpringBootRocketMqApplication      : Starting SpringBootRocketMqApplication on DESKTOP-HS6GR38 with PID 12496 (D:\wsforjava\springboot-rocketmq\target\classes started by suolo in D:\wsforjava\springboot-rocketmq)
2018-03-03 16:54:25.086  INFO 12496 --- [           main] c.c.c.SpringBootRocketMqApplication      : No active profile set, falling back to default profiles: default
2018-03-03 16:54:25.201  INFO 12496 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@236e3f4e: startup date [Sat Mar 03 16:54:25 CST 2018]; root of context hierarchy
2018-03-03 16:54:27.073  INFO 12496 --- [           main] c.c.c.r.p.MQProducerConfiguration        : producer is start ! groupName:[springboot-rocketmq],namesrvAddr:[47.97.8.22:9876]
2018-03-03 16:54:27.430  INFO 12496 --- [           main] c.c.c.r.c.MQConsumerConfiguration        : consumer is start !!! groupName:springboot-rocketmq,topics:DemoTopic~*;,namesrvAddr:47.97.8.22:9876
2018-03-03 16:54:27.747  INFO 12496 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-03-03 16:54:27.764  INFO 12496 --- [           main] c.c.c.SpringBootRocketMqApplication      : Started SpringBootRocketMqApplication in 3.213 seconds (JVM running for 3.814)
2018-03-03 16:54:27.784  INFO 12496 --- [MessageThread_1] .c.c.r.c.p.MQConsumeMsgListenerProcessor : 根據Topic:DemoTopic和Tag:DemoTag 路由到的服務爲:com.clouds.common.demo.DemoNewTopicConsumerMsgProcessImpl,開始調用處理消息
2018-03-03 16:54:30.162  INFO 12496 --- [MessageThread_1] c.c.c.r.c.p.AbstractMQMsgProcessor       : 獲取到的消息爲:demo msg test
2018-03-03 16:54:30.194  INFO 12496 --- [MessageThread_1] .c.c.r.c.p.MQConsumeMsgListenerProcessor : 消息處理成功:{"reconsumeLater":true,"saveConsumeLog":false,"success":true}

 

完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章