效果图:
文章目录
- 一、实现原理
- 二、代码实战
- 2.1. 错误收集对象
- 2.2. 模拟方法
- 2.3. aop拦截
- 2.4. 异常信息收集队列
- 2.5. 发送微信模板
- 2.6. 微信告警模板
- 2.7. redis存储图
- 2.8. 效果图
一、实现原理
1. 原理设计图示
2. 原理流程
1.浏览器或者app发起请求,在处理逻辑时发生异常,.
2.aop异常通知捕获,收集异常信息,存入队列中。
3.全局异常捕获,拼接错误json数据,将封装好的json返回前端
4.单独的线程每隔1秒,就去队列中获取异常消息。
5.调用微信公众号模板接口发送模板.
6.先判断此通知在1分钟之内是否已经发起通知,若发起,则流程结束。
7.若未发起,则,拼装模板异常信息调用微信公众号接口
8.推送告警通知。
二、代码实战
2.1. 错误收集对象
package com.mayikt.main.alarm.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 错误收集对象
*
* @author gblfy
* @Date 2022-09-13
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AlarmEntity {
/**
* 类的名称
*/
private String className;
/**
* 方法名称
*/
private String methodName;
/**
* 服务名称
*/
private String serviceName;
/**
* 服务IP
*/
private String ip;
/**
* 端口号码
*/
private String port;
/**
* 错误行号
*/
private int wrongLineNumber;
/**
* 错误内容
*/
private String errorMsg;
/**
* 发生错误时间
*/
private Date errorTime;
}
2.2. 模拟方法
@RestController
public class TestSecurityServiceImpl implements TestSecurityService {
@GetMapping("/insert")
@Override
public String insert(int age) {
int j = 1 / age;
return "insert"+j;
}
}
2.3. aop拦截
package com.mayikt.main.alarm;
import com.mayikt.main.alarm.entity.AlarmEntity;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Enumeration;
/**
* 告警通知aop
*
* @author gblfy
* @Date 2022-09-13
**/
@Slf4j
@Aspect
@Component
public class AopInterceptionError {
@Value("${spring.application.name}")
private String serviceName;
@Value("${server.port}")
private String serverPort;
/**
* 通过aop异常通知拦截系统日志
*/
@Pointcut("execution(public * com.mayikt.main.api.impl..*.*(..))")
public void aopInterceptionError() {
}
//异常通知:在方法出现异常时进行通知,可以范文异常防撞对象,且可以指定在出现特定异常时执行通知
@AfterThrowing(value = "aopInterceptionError()", throwing = "e")
public void afterThrowing(JoinPoint joinpoint, Exception e) throws UnknownHostException {
//aop 异常通知获取栈帧 方法名称 参数值 累的名称 等 ip 和端口号码 服务名称
log.info("joinpoint->{}", joinpoint);
//报错内容
String errorMsg = e.getMessage();
StackTraceElement stackTraceElement = e.getStackTrace()[0];
//类的名称
String className = stackTraceElement.getClassName();
//错误行号
int lineNumber = stackTraceElement.getLineNumber();
//方法名称
String methodName = stackTraceElement.getMethodName();
AlarmEntity alarmEntity = new AlarmEntity(className,
methodName,
serviceName,
getLocalHostLANAddress().getHostAddress(),
serverPort,
lineNumber,
errorMsg,
new Date());
//将该对象直接存放入队列中
AlarmContainer.addAlarm(alarmEntity);
}
/**
* ip获取方案:
* 第一种情况:非容器部署,直接获取服务器ip地址
* 第二种情况:docker容器部署,获取容器启动传递的参数即可
* docker run (加上参数==指定服务器ip地址)
*/
public InetAddress getLocalHostLANAddress() {
try {
InetAddress candidateAddress = null;
// 遍历所有的网络接口
for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) {
NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
// 在所有的接口下再遍历IP
for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
InetAddress inetAddr = (InetAddress) inetAddrs.nextElement();
if (!inetAddr.isLoopbackAddress()) {// 排除loopback类型地址
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址,就是它了
return inetAddr;
} else if (candidateAddress == null) {
// site-local类型的地址未被发现,先记录候选地址
candidateAddress = inetAddr;
}
}
}
}
if (candidateAddress != null) {
return candidateAddress;
}
// 如果没有发现 non-loopback地址.只能用最次选的方案
InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
return jdkSuppliedAddress;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
2.4. 异常信息收集队列
package com.mayikt.main.alarm;
import com.mayikt.main.alarm.entity.AlarmEntity;
import com.mayikt.main.manage.WechatTemplateManage;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.LinkedBlockingDeque;
/**
* 异常信息收集队列
*
* @author gblfy
* @Date 2022-09-13
**/
@Component
public class AlarmContainer {
@Autowired
private WechatTemplateManage wechatTemplateManage;
public AlarmContainer() {
new SendAlarmThread().start();
}
/**
* 缓存告警错误内容
*/
private static LinkedBlockingDeque<AlarmEntity> alarmEntityDeque = new LinkedBlockingDeque<>();
/**
* 添加告警
*/
public static void addAlarm(AlarmEntity alarmEntity) {
alarmEntityDeque.offer(alarmEntity);
}
// /**
// * 获取告警内容
// */
// public static AlarmEntity getAlarm() {
// return alarmEntityDeque.poll();
// }
//
// /**
// * 移除告警
// */
// public static void removeAlarm() {
// alarmEntityDeque.remove();
// }
class SendAlarmThread extends Thread {
@SneakyThrows
@Override
public void run() {
while (true) {
//获取告警内容
AlarmEntity alarmEntity = alarmEntityDeque.poll();
if (alarmEntity != null) {
// 调用微信公众号末班接口发送模板
wechatTemplateManage.sendAlarmTemplate(alarmEntity);
}
// 为了避免cpu飚高,延迟1s
Thread.sleep(1000);
}
}
}
}
2.5. 发送微信模板
/**
* 发送警告模板
*
* @param alarmEntity
* @return
*/
public boolean sendAlarmTemplate(AlarmEntity alarmEntity) {
/**
* 高并发避免重复发的方案
* redis key=服务名称+ip+端口号+类的名称+方法名称+错误行号 value=报错的参数值(内容) 设置过期时间比如:1分钟 一分钟内同样的报错只会通知一次
*
*/
String serviceName = alarmEntity.getServiceName();
String ip = alarmEntity.getIp();
String port = alarmEntity.getPort();
String className = alarmEntity.getClassName();
String methodName = alarmEntity.getMethodName();
int wrongLineNumber = alarmEntity.getWrongLineNumber();
String errorMsg = alarmEntity.getErrorMsg();
String errorRepeatKey = serviceName + "_" + ip + "_" + port + "_" + className + "_" + methodName + "_" + wrongLineNumber;
String errorRepeatValue = errorRepeatKey + "_" + errorMsg;
//判断key是否存在
String redisErrorMsg = RedisUtil.getString(errorRepeatKey);
if (!StringUtils.isEmpty(redisErrorMsg)) {
return false;
}
//将错误发送记录存储redis
RedisUtil.setString(errorRepeatKey, errorRepeatValue, 60L);
WxMpTemplateMessage wxMpTemplateMessage = new WxMpTemplateMessage();
wxMpTemplateMessage.setTemplateId(alarmTemplateId);
wxMpTemplateMessage.setToUser("oD8nF5jpNF9KXU_j49uvqOPVdiEU");
List<WxMpTemplateData> data = new ArrayList<>();
data.add(new WxMpTemplateData("first", serviceName));
data.add(new WxMpTemplateData("keyword1", ip + ":" + port));
data.add(new WxMpTemplateData("keyword2", "报错的类:" + className));
data.add(new WxMpTemplateData("keyword3", "报错的方法:" + methodName));
data.add(new WxMpTemplateData("keyword4", "报错的行号:" + wrongLineNumber));
data.add(new WxMpTemplateData("keyword5", "报错的内容:" + errorMsg));
data.add(new WxMpTemplateData("keyword6", SimpleDateFormatUtils.getFormatStrByPatternAndDate(alarmEntity.getErrorTime())));
wxMpTemplateMessage.setData(data);
//点击模板指定跳转地址
// wxMpTemplateMessage.setUrl("http://www.mayikt.com");
try {
String appId = wxMpProperties.getConfigs().get(0).getAppId();
System.out.println("appId" + appId);
WxMpTemplateMsgService templateMsgService = WxMpConfiguration.getMpServices().get(appId).getTemplateMsgService();
templateMsgService.sendTemplateMsg(wxMpTemplateMessage);
return true;
} catch (Exception e) {
log.error("e->{}", e);
e.printStackTrace();
return false;
}
}
2.6. 微信告警模板
模板标题:告警通知2
模板内容:
服务名称:{{first.DATA}}
IP和端口:{{keyword1.DATA}}
报错的类:{{keyword2.DATA}}
报错方法:{{keyword3.DATA}}
报错行号:{{keyword4.DATA}}
报错内容:{{keyword5.DATA}}
错误时间:{{keyword6.DATA}}
发生了系统错误,请您及时登录服务器查看并解决
模板ID(用于接口调用): _axxxxxxxxxxxxxxx3jMYdF4
2.7. redis存储图
2.8. 效果图