最近把項目裏的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參數用來再發生錯誤時調用來處理特別處理類似還原回滾這種操作。