前言
在微服务/分布式系统中,接口异常、mq消费失败是高频故障点。若依赖人工排查,会导致故障响应滞后、影响业务可用性。本文基于 spring aop 实现无侵入式异常告警,支持 钉钉机器人 + 企业微信机器人 双渠道切换,自动捕获:
- controller 层非业务异常
- rocketmq 消费失败(达到指定重试次数)
并实时推送告警消息,支持环境区分、@指定人、配置动态刷新,完全适配生产环境使用。
一、技术方案设计
核心目标
- 无侵入:通过 aop 切面拦截,不污染业务代码
- 双渠道:钉钉/企业微信自由切换,支持关闭告警
- 精准告警:controller 过滤业务异常,mq 仅在最大重试次数告警
- 生产可用:异步发送告警、配置动态刷新、日志完整、消息长度截断
技术栈
- spring boot + spring aop
- rocketmq (mq消费告警)
- 钉钉/企业微信 群机器人 webhook
- 异步线程池(避免告警阻塞主流程)
- nacos 配置动态刷新
二、完整优化代码(带详细注释)
1. 核心配置类(告警参数动态配置)
负责加载钉钉/企微机器人配置,支持 @refreshscope 动态刷新(nacos/apollo)
package com.xm.kite.tms.common.config;
import org.springframework.beans.factory.annotation.value;
import org.springframework.cloud.context.config.refreshscope;
import org.springframework.context.annotation.configuration;
import java.io.serial;
import java.io.serializable;
import java.util.list;
/**
* tms业务通用配置类
* 核心作用:加载钉钉/企微告警机器人配置,支持配置中心动态刷新
*/
@configuration
@refreshscope
public class tmsbusinessconfig implements serializable {
@serial
private static final long serialversionuid = 2911611355174552504l;
/**
* 当前运行环境(dev/test/prod)
*/
@value("${spring.profiles.active}")
private string env;
// ====================== 钉钉机器人配置 ======================
/**
* 钉钉机器人webhook请求地址(占位符替换token)
*/
@value("${ding.ding.robot.webhook.url:https://oapi.dingtalk.com/robot/send?access_token=%s}")
private string dingdingrobotwebhookurl;
/**
* 钉钉机器人访问令牌
*/
@value("${ding.ding.robot.webhook.access_token:}")
private string dingdingrobotwebhookaccesstoken;
/**
* 钉钉告警@指定人手机号(多个用英文逗号分隔)
*/
@value("${ding.ding.robot.webhook.atmobiles:}")
private string dingdingrobotwebhookatmobiles;
// ====================== 通用告警通道配置 ======================
/**
* 告警通道开关:0-关闭 1-钉钉 2-企业微信
*/
@value("${robot.webhook.channel:2}")
private integer robotwebhookchannel;
// ====================== 企业微信机器人配置 ======================
/**
* 企微机器人webhook请求地址(占位符替换key)
*/
@value("${qy.weixin.robot.webhook.url:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s}")
private string qyweixinrobotwebhookurl;
/**
* 企微机器人密钥
*/
@value("${qy.weixin.robot.webhook.key:}")
private string qyweixinrobotwebhookkey;
/**
* 企微告警@指定人账号(多个用英文逗号分隔)
*/
@value("#{'${qy.weixin.robot.webhook.at_user_ids:}'.split(',')}")
private list<string> qyweixinrobotwebhookatuserids;
/**
* 企微运营数据播报专用webhook地址
*/
@value("${qy.weixin.robot.tmsdata.url:}")
private string qyweixintmsdatakurl;
// ====================== getter ======================
public string getenv() {return env;}
public string getdingdingrobotwebhookurl() {return dingdingrobotwebhookurl;}
public string getdingdingrobotwebhookaccesstoken() {return dingdingrobotwebhookaccesstoken;}
public string getdingdingrobotwebhookatmobiles() {return dingdingrobotwebhookatmobiles;}
public integer getrobotwebhookchannel() {return robotwebhookchannel;}
public string getqyweixinrobotwebhookurl() {return qyweixinrobotwebhookurl;}
public string getqyweixinrobotwebhookkey() {return qyweixinrobotwebhookkey;}
public list<string> getqyweixinrobotwebhookatuserids() {return qyweixinrobotwebhookatuserids;}
public string getqyweixintmsdatakurl() {return qyweixintmsdatakurl;}
}2. aop 异常拦截切面(核心)
无侵入拦截 controller层 和 mq消费层 异常,过滤业务异常,触发告警
package com.xm.kite.tms.common.config.aop;
import com.alibaba.fastjson.json;
import com.xm.kite.tms.common.config.tmsbusinessconfig;
import com.xm.kite.tms.pda.util.dingdingutils;
import lombok.extern.slf4j.slf4j;
import org.apache.commons.lang3.stringutils;
import org.apache.commons.lang3.exception.exceptionutils;
import org.apache.rocketmq.common.message.messageext;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.pointcut;
import org.aspectj.lang.reflect.methodsignature;
import org.hbhk.hms.mq.tx.mq.mqmsgvo;
import org.springframework.stereotype.component;
import javax.annotation.resource;
import java.lang.reflect.method;
/**
* 系统异常告警aop切面
* 核心功能:
* 1. 拦截controller层所有非业务异常,推送告警
* 2. 拦截mq消费异常,达到最大重试次数后推送告警
*/
@aspect
@component
@slf4j
public class aopfailnoticehandler {
@resource
private dingdingutils dingdingutils;
@resource
private tmsbusinessconfig tmsbusinessconfig;
// ====================== 切点定义 ======================
/**
* 切点:pda模块所有controller
*/
@pointcut("within(com.xm.kite.tms.pda.controller..*)")
protected void aopfailnotice() {}
/**
* 切点:vms模块所有controller
*/
@pointcut("within(com.xm.kite.tms.vms.controller..*)")
protected void aopvmsfailnotice() {}
/**
* 切点:所有mq消费类
*/
@pointcut("within(com.xm.kite.tms.pda.mq..*)")
protected void mqfailnotice() {}
// ====================== controller异常拦截 ======================
/**
* 环绕通知:拦截controller层所有方法异常
* 过滤业务异常,仅推送系统异常告警
*/
@around("aopfailnotice() || aopvmsfailnotice()")
public object controllerfailnoticearound(proceedingjoinpoint jp) throws throwable {
try {
// 执行目标方法
return jp.proceed();
} catch (throwable e) {
// 获取异常方法全路径
string targetclassname = jp.gettarget().getclass().getname();
string methodname = ((methodsignature) jp.getsignature()).getname();
string fullmethodname = targetclassname + "." + methodname;
// 获取请求参数
string params = json.tojsonstring(jp.getargs());
// 过滤:业务异常不发送告警
if (!(e instanceof businessexception || e instanceof bizexception)) {
log.error("【controller异常】方法:{},参数:{}", fullmethodname, params, e);
// 截取异常堆栈(避免消息过长)
string errorstack = stringutils.left(exceptionutils.getstacktrace(e), 500);
// 发送双渠道告警
dingdingutils.buildmsg(errorstack, fullmethodname, params);
}
// 抛出异常,不改变原有异常流程
throw e;
}
}
// ====================== mq消费异常拦截 ======================
/**
* 环绕通知:拦截mq消费方法异常
* 达到指定重试次数后发送告警
*/
@around("mqfailnotice()")
public object mqconsumerfailnoticearound(proceedingjoinpoint joinpoint) throws throwable {
// 解析mq消息参数
object[] args = joinpoint.getargs();
int reconsumetimes = 0;
string messageid = "";
messageext messageext = null;
mqmsgvo mqmsgvo = null;
// 解析消息体、重试次数、消息id
if (args != null) {
// 解析rocketmq原生消息
if (args[0] instanceof messageext ext) {
messageext = ext;
reconsumetimes = ext.getreconsumetimes() + 1;
messageid = ext.getproperty("uniq_key");
}
// 解析自定义mq消息体
if (args.length > 1 && args[1] instanceof mqmsgvo vo) {
mqmsgvo = vo;
}
}
try {
// 执行mq消费逻辑
return joinpoint.proceed();
} catch (exception e) {
log.info("【mq消费失败】次数:{},消息id:{}", reconsumetimes, messageid);
// 自定义最大告警次数:消费失败5次触发告警
final int max_retry_times = 5;
if (reconsumetimes == max_retry_times) {
log.error("【mq消费告警】达到最大重试次数,消息id:{}", messageid);
string errormsg = stringutils.left(e.getmessage(), 200);
// 发送mq消费失败告警
dingdingutils.buildandsenddingalarmrobotmsg(messageext, mqmsgvo, messageid, reconsumetimes, errormsg);
}
// 抛出异常,让mq继续重试
throw e;
}
}
}3. 告警工具类(钉钉+企微双渠道)
封装消息模板、http发送、异步处理、@人逻辑,支持markdown格式告警
package com.xm.kite.tms.pda.util;
import cn.hutool.core.collection.collectionutil;
import cn.hutool.core.date.dateutil;
import cn.hutool.core.util.objectutil;
import com.alibaba.fastjson.json;
import com.alibaba.fastjson.jsonobject;
import com.xm.kite.tms.common.config.tmsbusinessconfig;
import com.xm.kite.tms.pda.vo.dingding.senddingalarmrobotmsgdto;
import com.xm.kite.tms.pda.vo.dingding.qyweixin.qyweixinsenddingalarmrobotmsgdto;
import lombok.extern.slf4j.slf4j;
import org.apache.commons.lang3.stringutils;
import org.apache.rocketmq.common.message.messageext;
import org.springframework.cloud.context.config.annotation.refreshscope;
import org.springframework.http.httpentity;
import org.springframework.http.httpheaders;
import org.springframework.http.mediatype;
import org.springframework.http.responseentity;
import org.springframework.stereotype.component;
import org.springframework.web.client.resttemplate;
import javax.annotation.resource;
import java.text.messageformat;
import java.util.arrays;
import java.util.list;
import java.util.concurrent.executor;
/**
* 钉钉+企业微信 告警机器人工具类
* 核心功能:
* 1. 支持双渠道切换发送markdown告警消息
* 2. 异步发送,不阻塞业务主线程
* 3. 自动@指定人员,格式化消息模板
*/
@slf4j
@component
@refreshscope
public class dingdingutils {
// 通道常量
public static final integer channel_close = 0;
public static final integer channel_ding = 1;
public static final integer channel_wechat = 2;
// 消息类型
public static final string markdown = "markdown";
// ====================== 告警消息模板 ======================
/** mq消费失败告警模板 */
public static final string mq_fail_template = """
# **{0}**-**tms-mq消费失败告警**
\n**告警时间**: <font color="#ff0000">**{1}**</font>
\n**topic**: <font color="#ff0000">**{2}**</font>
\n**messageid**: <font color="#ff0000">**{3}**</font>
\n**失败原因**: <font color="#ff0000">**{4}**</font>
\n**消息参数**: {5}
""";
/** controller通用异常模板 */
public static final string controller_fail_template = """
# **{0}**-**tms-接口异常告警**
\n**告警时间**: <font color="#ff0000">**{1}**</font>
\n**异常信息**: <font color="red">**{2}**</font>
\n**异常方法**: <font color="#32cd32">{3}</font>
\n**请求参数**: {4}
""";
@resource
private executor baseexecutor;
@resource
private tmsbusinessconfig config;
private final resttemplate resttemplate = new resttemplate();
// ====================== 对外暴露方法 ======================
/**
* 发送controller异常告警
*/
public void buildmsg(string errormsg, string methodname, string params) {
if (channel_close.equals(config.getrobotwebhookchannel())) {
log.warn("告警通道已关闭");
return;
}
// 格式化消息
string content = messageformat.format(controller_fail_template,
config.getenv(), dateutil.now(), errormsg, methodname, stringutils.left(params, 3500));
// 分发到对应渠道
sendbychannel(content);
}
/**
* 发送mq消费失败告警
*/
public void buildandsenddingalarmrobotmsg(messageext messageext, mqmsgvo mqmsgvo, string messageid, int times, string errormsg) {
try {
if (channel_close.equals(config.getrobotwebhookchannel())) return;
errormsg = stringutils.defaultifblank(errormsg, "未知异常");
string content = messageformat.format(mq_fail_template,
config.getenv(), dateutil.now(), messageext.gettopic(), messageid,
errormsg, stringutils.left(json.tojsonstring(mqmsgvo), 3500));
sendbychannel(content);
} catch (exception e) {
log.error("构建mq告警消息异常", e);
}
}
// ====================== 渠道分发核心 ======================
/**
* 根据配置自动分发到钉钉/企微
*/
private void sendbychannel(string content) {
if (channel_ding.equals(config.getrobotwebhookchannel())) {
senddingalarmrobotmsgdto dto = senddingalarmrobotmsgdto.buildmarkdown(content);
sendalarmrobotmsg(dto, true);
}
if (channel_wechat.equals(config.getrobotwebhookchannel())) {
qyweixinsenddingalarmrobotmsgdto dto = qyweixinsenddingalarmrobotmsgdto.builddto(config.getqyweixinrobotwebhookatuserids(), content);
sendqyalarmrobotmsg(dto, true);
}
}
// ====================== 底层发送逻辑(已省略冗余代码) ======================
/**
* 异步发送钉钉消息
*/
public void sendalarmrobotmsg(senddingalarmrobotmsgdto msgdto, boolean isasync) {
if (isasync) {
baseexecutor.execute(() -> senddinghttp(msgdto));
}
}
/**
* 异步发送企微消息
*/
private void sendqyalarmrobotmsg(qyweixinsenddingalarmrobotmsgdto msgdto, boolean isasync) {
if (isasync) {
baseexecutor.execute(() -> sendqyhttp(msgdto));
}
}
// 钉钉http请求
private webresponse<void> senddinghttp(senddingalarmrobotmsgdto dto) {
// 原http发送逻辑(保留不变)
return webresponseutil.success.build();
}
// 企微http请求
private webresponse<void> sendqyhttp(qyweixinsenddingalarmrobotmsgdto dto) {
// 原http发送逻辑(保留不变)
return webresponseutil.success.build();
}
}三、配置文件(application.yml)
# 环境配置
spring:
profiles:
active: prod
# 钉钉机器人
ding:
ding:
robot:
webhook:
url: https://oapi.dingtalk.com/robot/send?access_token=%s
access_token: xxxxx
atmobiles: 13800138000
# 企业微信机器人
qy:
weixin:
robot:
webhook:
url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s
key: xxxxx
at_user_ids: user1,user2
# 告警通道配置
robot:
webhook:
channel: 2 # 0关闭 1钉钉 2企微四、核心功能与优化点
1. 核心能力
- 双渠道自由切换:配置文件一键切换钉钉/企微,支持关闭告警
- 精准告警
- controller:仅拦截系统异常,业务异常不骚扰
- mq:仅在重试5次后告警,避免重复通知
- 异步发送:线程池异步推送告警,不阻塞接口/mq消费
- 动态配置:支持nacos配置热刷新,无需重启服务
- 消息格式化:markdown 排版,自动截断超长消息
- @指定人:支持钉钉@手机号、企微@用户账号
2. 代码优化点
- 统一常量定义,消除魔法值
- 抽取公共方法,减少代码冗余
- 完整日志打印,方便问题排查
- 空指针防护、参数校验
- 规范注释,类/方法/关键逻辑全覆盖
- 环境区分,告警自带环境标识(prod/test/dev)
五、生产环境注意事项
- 线程池隔离:告警异步线程建议使用独立线程池,避免占用业务线程
- 消息限流:高并发异常时,防止告警消息轰炸群聊
- 异常过滤:严格过滤业务异常,仅监控系统异常(空指针、sql异常等)
- 配置安全:机器人token/key不要硬编码,放入配置中心加密存储
- 重试机制:告警http请求可增加简单重试,提升送达率
六、实现效果


总结
本文基于 spring aop 实现了一套生产级、无侵入、双渠道的异常告警方案,完美覆盖 controller接口异常 和 mq消费失败 两大核心场景。
- 对业务代码零侵入,接入成本极低
- 支持钉钉/企业微信自由切换
- 异步、配置化、可动态刷新
完全满足分布式系统的实时故障监控需求,大幅提升故障响应效率!
到此这篇关于springboot aop实现钉钉+企业微信双渠道异常告警的文章就介绍到这了,更多相关springboot aop双渠道异常告警内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论