RocketMQ消息鏈路路由的一個實現

最近把項目裏的ActiveMQ改成了RocketMQ,改的時候發現RocketMQ好像沒有什麼現成的鏈式訪問路由的配置(當然也可能是我沒搜索到,問了一下羣裏也沒有明確的說法),所以自己實現了一套。這個東西之前是存再mysql裏的(爲了可以回查所有過程)這次改用隊列方式實現存日誌來回查。

一、介紹這個東西之前先講假設一個需求來看看這個東西能幹什麼,假設有個需求

1、用戶提交短信驗證碼

2、驗證碼驗證成功後保存用戶信息更新

3、調用發送短信通知更新成功等待系統審覈

4、調用郵件系統發送系統自動審覈結果

從上面的需求可以看到,這個鏈路至少要調用4個不同的接口,按照rocketmq開發文檔的說明,也就是做個消費端一路往下傳,但是做起來太麻煩,所以實現了一套基於rocketmq的鏈路訪問的方法。

二、接下去看一下要完成上面的需求新組件要怎麼做

@RocketRoute  //標記這個類或接口裏有消息路由接口
@FeignClient("project-authority")
@RequestMapping(value = "/authority")
@RestController
public interface AuthorityInterface {
  
   //通過RouteMethod標記路由,此接口會處理對應配置的消息,next表明下一層路由的tags值
   //此接口的返回值會存入對應next參數的tags隊列裏,只要配置了next就會一路流轉下去
   @RouteMethod(topic = "auth", tags = "login", next = "edit") 
   @PostMapping("/MobileCodeCheck")
   ResultBase<LoginResult> MobileCodeCheck(@RequestBody RequestBase<SmsRequest> entity);

   @RouteMethod(topic = "auth", tags = "edit", next = "sms") 
   @PostMapping("/EditUserInfo")
   ResultBase<UserResult> EditUserInfo(@RequestBody RequestBase<LoginResult> entity);

   @RouteMethod(topic = "auth", tags = "sms", next = "email") 
   @PostMapping("/SendEditSms")
   ResultBase<SendSmsResult> SendEditSms(@RequestBody RequestBase<UserResult> entity);

   @RouteMethod(topic = "auth", tags = "email") 
   @PostMapping("/SendEmail")
   ResultBase<SendSmResult> SendEmail(@RequestBody RequestBase<SendSmsResult> entity);

}

上面就是一個典型的鏈路調用過程(方法忽略不用管),調用過程是

先有某個程序向autho.login這個隊列裏發送了一條消息,消息會由消費者推給MobileCodeCheck,此接口返回的值會發送到next裏定義的tags(這裏是edit)下面以此類推。

MobileCodeCheck(login) -> EditUserInfo(edit) -> SendEditSms(sms) -> SendEmail(email)

整個鏈路由系統自動完成,每個接口的入參就是上一個接口的出參

大概理解了這個東西是幹什麼的以後,我把所有代碼都貼出來,具體放那裏隨意,我是放在coumser裏的。代碼我就不放公共git了因爲裏面有太多業務代碼,直接貼代碼出來將就的看看。

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

//這個就是個標記類沒有任何內容,我再啓動時會查找所有使用此標記的類來獲取路由
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RocketRoute {
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//這個用來標記路由具體調用的服務
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouteMethod {
    String topic();  //rocketmq的topic
    String tags();   //tags值
    //同步異步設置,消費端接到消息後如果設置爲異步會創建一個mono來調用
    boolean async() default false;  
    //默認情況下程序會將隊列裏的值(json)轉成RocketValueEntity類型
    //因爲裏面包含了taskId和參數列表(數組)
    //如果你設置了程序就認爲入參只有一個而且用的類進行json轉換,設置的類必須再bean裏
    String className() default "";
    //下一步路由的tags
    String next() default "";
}

//默認存隊列的數據結構
public class RocketValueEntity {
    private String taskId;   //一個標記消息的唯一標誌
    private String[] parameter; //調用接口參數列表

    public String getTaskId() {
        return taskId;
    }

    public void setTaskId(String taskId) {
        this.taskId = taskId;
    }

    public String[] getParameter() {
        return parameter;
    }

    public void setParameter(String[] parameter) {
        this.parameter = parameter;
    }
}
import com.project.thisConsume.common.rocketmq.annotation.RocketRoute;
import com.project.thisConsume.common.rocketmq.annotation.RouteMethod;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class RocketRouteSetting implements ApplicationContextAware {

    private static ApplicationContext applicationValue;
    private static Map<String, List<RocketRouteBean>> serverContext = new HashMap();

    @Override
    public void setApplicationContext(ApplicationContext applicationcontext)
            throws BeansException {
        applicationValue = applicationcontext;
        String[] beanNamesForAnnotation = applicationcontext.getBeanNamesForAnnotation(RocketRoute.class);
        for(String entry : beanNamesForAnnotation){
            Class<?> type = applicationcontext.getType(entry);
            Method[] methods = type.getMethods();
            for (Method m : methods){
                if (m.isAnnotationPresent(RouteMethod.class)){
                    RouteMethod val = m.getAnnotation(RouteMethod.class);
                    RocketRouteBean rocketRoutBean = new RocketRouteBean();
                    rocketRoutBean.setBean(applicationcontext.getBean(entry));
                    rocketRoutBean.setMethod(m);
                    rocketRoutBean.setAsync(val.async());
                    rocketRoutBean.setClassName(val.className());
                    rocketRoutBean.setResult(val.next());
                    rocketRoutBean.setRouteMethod(val);
                    rocketRoutBean.setName(String.format("%s.%s", val.topic(), val.tags()));
                    if (!serverContext.containsKey(rocketRoutBean.getName())){
                        serverContext.put(rocketRoutBean.getName(), new ArrayList<>());
                    }
                    serverContext.get(rocketRoutBean.getName()).add(rocketRoutBean);
                }
            }
        }
    }

    public static Class<?> getBeanType(String name){
        return applicationValue.getType(name);
    }

    public static List<RocketRouteBean> getBean(String name) throws InvocationTargetException, IllegalAccessException {
        if (serverContext.containsKey(name)) {
            return serverContext.get(name);
        }
        return null;
    }

    public static Map<String, List<RocketRouteBean>> getBeans(){
        return serverContext;
    }
}
import com.alibaba.fastjson.JSONObject;
import com.project.thisConsume.common.rocketmq.annotation.RouteMethod;
import org.apache.commons.lang.StringUtils;
import reactor.core.publisher.Mono;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.CompletableFuture;

//存路由信息的bean
public class RocketRouteBean {
    private String name;
    private Method method;
    private Object bean;
    private String className;
    private boolean async;
    private String result;
    private RouteMethod routeMethod;

    public RouteMethod getRouteMethod() {
        return routeMethod;
    }

    public void setRouteMethod(RouteMethod routeMethod) {
        this.routeMethod = routeMethod;
    }

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public boolean isAsync() {
        return async;
    }

    public void setAsync(boolean async) {
        this.async = async;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Object getBean() {
        return bean;
    }

    public void setBean(Object bean) {
        this.bean = bean;
    }

    private Object[] parameters(String args, String cls){
        Method method = getMethod();
        Class<?>[] parameterTypes = method.getParameterTypes();
        Object[] inputs;
        if (StringUtils.isBlank(cls)){
            RocketValueEntity rocketValueEntity = JSONObject.parseObject(args, RocketValueEntity.class);
            //查找方法所有參數列表轉json
            inputs = new Object[rocketValueEntity.getParameter().length];
            for (int i=0;i<parameterTypes.length;i++)
                inputs[i] = JSONObject.parseObject(rocketValueEntity.getParameter()[i], parameterTypes[i]);
        } else {
            //如果你配值了className值就直接用的name從bean裏查找對象
            inputs = new Object[1];
            Class<?> type = RocketRouteSetting.getBeanType(cls);
            inputs[1] = JSONObject.parseObject(args, type);
        }

        return inputs;
    }

    public String invoke(String args, String cls) throws InvocationTargetException, IllegalAccessException {
        Object obj = getMethod().invoke(getBean(), parameters(args, cls));
        return JSONObject.toJSONString(obj);
    }

    public String invoke(String args) throws InvocationTargetException, IllegalAccessException {
        return invoke(args, getClassName());
    }

    public Mono<String> syncInvoke(String args){
        return syncInvoke(args, getClassName());
    }

    public Mono<String> syncInvoke(String args, String cls){
        //如果是異步就啓動一個mono
        return Mono.fromFuture(CompletableFuture.supplyAsync (()->{
            try
            {
                Object obj = getMethod().invoke(getBean(), parameters(args, cls));
                return JSONObject.toJSONString(obj);
            } catch (Exception e){
                throw new RuntimeException(e);
            }
        }));
    }

}
import com.project.thisConsume.common.rocketmq.executor.RocketMqProductExecutor;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RocketMqListenerConsumer implements MessageListenerConcurrently {

    @Autowired
    private RocketMqProductExecutor rocketMqProduct;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try
        {
            for(MessageExt msg : list){
                String name = String.format("%s.%s", msg.getTopic(), msg.getTags());
                List<RocketRouteBean> bean = RocketRouteSetting.getBean(name);
                for (RocketRouteBean item : bean){
                    if (item.isAsync()){
                        item.syncInvoke(new String(msg.getBody())).subscribe((json) -> rocketMqProduct.send(item.getRouteMethod().topic(), item.getResult(), json));
                    } else {
                        rocketMqProduct.send(item.getRouteMethod().topic(), item.getResult(), item.invoke(new String(msg.getBody())));
                    }
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }

        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }


}
import com.project.thisConsume.common.rocketmq.RocketMqListenerConsumer;
import com.project.thisConsume.common.rocketmq.RocketRouteBean;
import com.project.thisConsume.common.rocketmq.RocketRouteSetting;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;

import java.util.List;
import java.util.Map;

@SpringBootConfiguration
public class RocketConsumerExecutor {

    @Value("${rocketmq.namesrv}")
    private String namesrvAddr;
    @Value("${rocketmq.group}")
    private String groupName;
    @Value("${rocketmq.threadMin}")
    private int threadMin;
    @Value("${rocketmq.threadMax}")
    private int threadMax;
    @Value("${rocketmq.batchMaxSize}")
    private int batchMaxSize;

    @Autowired
    RocketMqListenerConsumer rocketMqListenerConsumer;

    @Bean
    public DefaultMQPushConsumer rocketConsumer(){
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
        consumer.setNamesrvAddr(namesrvAddr);
        consumer.setConsumeThreadMin(threadMin);
        consumer.setConsumeThreadMax(threadMax);
        consumer.registerMessageListener(rocketMqListenerConsumer);
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.setMessageModel(MessageModel.BROADCASTING);
        consumer.setConsumeMessageBatchMaxSize(batchMaxSize);

        Map<String, List<RocketRouteBean>> beans = RocketRouteSetting.getBeans();
        try
        {
            for (Map.Entry<String, List<RocketRouteBean>> map : beans.entrySet()) {
                for (RocketRouteBean bean : map.getValue()){
                    consumer.subscribe(bean.getRouteMethod().topic(), bean.getRouteMethod().tags());
                    break;
                }
            }
            consumer.start();
        } catch (Exception e){
            e.printStackTrace();
        }

        return consumer;
    }
}
import com.alibaba.fastjson.JSONObject;
import com.project.thisConsume.common.rocketmq.RocketValueEntity;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.UUID;

//生產者
@Component
public class RocketMqProductExecutor {

    @Value("${rocketmq.namesrv}")
    private String namesrvAddr;
    /**
     * 消息最大大小,默認4M
     */
    @Value("${rocketmq.maxMessageSize}")
    private Integer maxMessageSize ;
    /**
     * 消息發送超時時間,默認3秒
     */
    @Value("${rocketmq.timeout}")
    private Integer sendMsgTimeout;
    /**
     * 消息發送失敗重試次數,默認2次
     */
    @Value("${rocketmq.retryTimes}")
    private Integer retryTimesWhenSendFailed;

    @Value("${rocketmq.group}")
    private String groupName;

    private boolean showdown = true;

    private DefaultMQProducer defaultMQProducer;

    protected synchronized void init(){
        if (null == defaultMQProducer){
            defaultMQProducer = new DefaultMQProducer(groupName);
            defaultMQProducer.setNamesrvAddr(this.namesrvAddr);
            //如果需要同一個jvm中不同的producer往不同的mq集羣發送消息,需要設置不同的instanceName
            //producer.setInstanceName(instanceName);
            defaultMQProducer.setMaxMessageSize(this.maxMessageSize);
            defaultMQProducer.setSendMsgTimeout(this.sendMsgTimeout);
            //如果發送消息失敗,設置重試次數,默認爲2次
            defaultMQProducer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);

            try {
                defaultMQProducer.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
            showdown = false;
        }
    }

    public synchronized void shutdown(){
        if (null != defaultMQProducer && !showdown){
            defaultMQProducer.shutdown();
            showdown = true;
        }
    }

    //最好用這個方法發,因爲他會包一層RocketValueEntity
    public void send(String topic, String result, String... jsons){
        init();
        RocketValueEntity rocketValueEntity = new RocketValueEntity();
        rocketValueEntity.setParameter(new String[jsons.length]);
        rocketValueEntity.setTaskId(UUID.randomUUID().toString());
        for(int i=0;i<jsons.length;i++){
            rocketValueEntity.getParameter()[i] = jsons[i];
        }
        String json = JSONObject.toJSONString(rocketValueEntity);
        Message sendMsg = new Message(topic, result, json.getBytes());
        try {
            defaultMQProducer.send(sendMsg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

最後說說問題,現在最大的問題是鏈式調用事務的問題,走到某一個地方斷了很麻煩,我的想法是會再註釋頭裏增加一個error參數用來再發生錯誤時調用來處理特別處理類似還原回滾這種操作。

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