1这个项目经过很多人迭代,期间Rabbitmq的监听方法也是各式各样的,到我手上需要解决一个问题,第三方应用将消息推送到我们的Rabbitmq中处理,可能存在各种原因导致消息处理失败,这个时候需要将处理失败的消息记录日志并重新处理,以前消息量少的时候只需要人工将消息体重新打入队列即可,但是现在消息多了这个方法已经淘汰了,于是我做了这个切面日志.
1.首先,用注解的方式作为日志切点
创建一个自定义注解(我没有办法从切面传递一些必须参数,所以通过注解的方式传递每个方法的参数)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiPromoteMessageLog {
// 服务名称
String app_name() default "";
// 队列名称
String queue_name() default "";
// 交换机名称
String exchange_name() default "";
// 路由名称
String routing_key() default "";
}
2.创建切面类
@Component
@Aspect
@EnableAsync
@Slf4j
public class AspectPromoteMessageLog {
@Resource
private ObjectMapper objectMapper;
/**
* 消息进接收到(未处理)
*/
private static final Integer MSG_RES_NO = 0;
/**
* 消息处理成功
*/
private static final Integer MSG_RES_OK = 1;
/**
* 消息处理异常
*/
private static final Integer MSG_RES_FAIL = 2;
/**
* 异常消息二次处理成功
*/
private static final Integer MSG_RES_FAIL_OK = 3;
@Pointcut("@annotation(com.xxx.xxx.log.aop.ApiPromoteMessageLog)")
public void point() {
}
/**
* 日志前置通知
*
* @param jp 切点参数
*/
@Before("point()")
public void before(JoinPoint jp) {
Object[] params = jp.getArgs();
Object param = params[0];
JSONObject jsonObject = objectMapper.convertValue(param, JSONObject.class);
//获取消息信息
HashMap<String, String> traceMap = new HashMap<>();
traceMap.put("class", jp.getTarget().getClass().getName());
traceMap.put("method", jp.getSignature().getName());
String dataKey = jsonObject.getString("dataKey");
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
ApiPromoteMessageLog annotation = method.getAnnotation(ApiPromoteMessageLog.class);
String appName = annotation.app_name();
String queueName = annotation.queue_name();
String exchangeName = annotation.exchange_name();
String routingKet = annotation.routing_key();
// 日志记录
BossPromoteMessageLog messageLog =
new BossPromoteMessageLog(appName, queueName, exchangeName, routingKet, dataKey);
messageLog.setBody(JSONObject.toJSONString(param));
// 默认未执行通过
messageLog.setStatus(MSG_RES_NO);
messageLog.setTrace(JSONObject.toJSONString(traceMap));
messageLog.setCreateTime(LocalDateTime.now());
messageLog.setUpdateTime(LocalDateTime.now());
log.info("消息处理对象{}", messageLog);
logFeign.saveLog(messageLog);
}
/**
* 日志返回通知
* <p>
* 整个方法执行成功完成
*
* @param jp 切点参数
*/
@AfterReturning("point()")
public void afterReturning(JoinPoint jp) {
...
}
/**
* 日志异常通知
* <p>
* 方法执行过程中存在异常执行
*
* @param jp 切点参数
*/
@AfterThrowing(pointcut = "point()", throwing = "e")
public void afterThrowing(JoinPoint jp, Exception e) {
log.error("队列消息处理异常信息{}", e.getMessage());
e.printStackTrace();
...
throw new MessageConversionException("消息消费失败,移出消息队列,不再试错");
}
}
1.在切面类中要先创建一个切入点
也就是
@Pointcut("@annotation(com.xxx.xxx.log.aop.ApiPromoteMessageLog)")
public void point() {
}
2.其中com.xxx.xxx.log.aop.ApiPromoteMessageLog就是自定义注解的路径
3.后续写的各种通知中的切点直接调用@Pointcut注解的方法即可,避免重复写一样的路径过于麻烦
4.JoinPoint
除了环绕通知外其他的通知都有JoinPoint参数,我们可以通过这个参数拿到自定义注解标记的方法的入参,详细自己看代码或者自己尝试,毕竟我也是一个一个获取看的
注意点!!!
Rabbitmq的方法入参我碰到的有两种,有的人用的是Map接收,有的人用的是JSONObject接收,队列中的数据都是json格式所以他们都能拿到所有参数,但是但是但是,我们从消息体获取数据的时候会受到影响,具体什么样的建议你自己试试,所以我这里直接用了spring提供的ObjectMapper进行反序列化成JSONObject.
5.异常通知
异常通知的入参除了JoinPoint还有一个异常类,需要注意的是@AfterThrowing(pointcut = "point()", throwing = "e")这里的e需要与方法入参的异常名一致.
6.抛出MessageConversionException异常
异常日志记录完毕之后我们需要手动抛出MessageConversionException异常,这样消息队列中的消息就会被手动消费掉,而不会一直卡在那里一直报错
3.使用
@ApiPromoteMessageLog(app_name = "web-server", queue_name = "testQueue", exchange_name = "testExchange", routing_key = "promote.test")
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "testQueue"), exchange = @Exchange(value = "testExchange", type = ExchangeTypes.TOPIC), key = "promote.test"))
public void testRabbitMq(Map<String, String> params) {
log.info("消费数据:{}", params);
try {
String dataKey = String.valueOf(params.get("dataKey"));
int num = Integer.parseInt(String.valueOf(params.get("num")));
log.info("dataKey:{}", dataKey);
log.info("num:{}", 100 / num);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
我们在这里try catch不会影响切面的异常通知,需要注意的是这个异常捕获之后必须抛出原异常,不能new 一个新的异常抛出,否则异常通知记录不到真实异常位置,记录的是抛出新异常的位置.
用这种方式解决消息队列错误消息日志问题,最后的处理没有给出来,我觉得逻辑还存在一些问题,
方案是写一个定时任务,定时处理日志状态为2也就是处理错误的日志,处理完成之后会修改状态,我用了这几个注解
@Async
@Scheduled(cron = "0 0/10 * * * ?")
@Transactional(rollbackFor = Exception.class)
第一个异步处理
第二个定时逻辑(每十分钟执行一次,详细自查)
第三个事务注解
最后:try catch的异常捕获处理比aop的异常通知先进行.
实习快完事了,慢慢学习混日子,拜拜喽