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参数用来再发生错误时调用来处理特别处理类似还原回滚这种操作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值