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