前言
在我們開發過程中,出現bug是非常常見的,不會說產品一旦上線就沒有bug,出現bug沒關係,關鍵是需要能夠及時發現異常。
當工程基本完成,開始部署到生產環境上,線上的工程一旦出現異常時,開發團隊就需要主動感知異常並協調處理,當然人不能一天24小時去盯著線上工程,
所以就需要一種機制來自動化的對異常進行通知,並精確到誰負責的那塊程式碼。這樣會極大地方便後續的運維。
本專案的開發願景是為了給使用者線上上專案的問題排查方面能夠帶來幫助,簡單配置,做到真正的開箱即用,同時異常資訊儘量詳細,幫助開發者快速定位問題。
目前支援基於釘釘,企業微信和郵箱的異常通知
一、核心程式碼
這裡只展示一些核心程式碼,具體的完整程式碼可以看github: spring-boot-exception-notice
1、異常資訊資料model
@Data
public class ExceptionInfo {
/** 工程名 */
private String project;
/** 異常的標識碼 */
private String uid;
/** 請求地址 */
private String reqAddress;
/** 方法名 */
private String methodName;
/** 方法引數資訊 */
private Object params;
/** 類路徑*/
private String classPath;
/** 異常資訊 */
private String exceptionMessage;
/** 異常追蹤資訊*/
private List<String> traceInfo = new ArrayList<>();
/** 最後一次出現的時間 */
private LocalDateTime latestShowTime = LocalDateTime.now();
}
2、異常捕獲切面
@Aspect
@RequiredArgsConstructor
public class ExceptionListener {
private final ExceptionNoticeHandler handler;
@AfterThrowing(value = "@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller) || @within(com.jincou.core.aop.ExceptionNotice)",
throwing = "e")
public void doAfterThrow(JoinPoint joinPoint, Exception e) {
handler.createNotice(e, joinPoint);
}
}
3、異常資訊通知配置類
@Configuration
@ConditionalOnProperty(prefix = ExceptionNoticeProperties.PREFIX, name = "enable", havingValue = "true")
@EnableConfigurationProperties(value = ExceptionNoticeProperties.class)
public class ExceptionNoticeAutoConfiguration {
private final RestTemplate restTemplate = new RestTemplate();
@Autowired(required = false)
private MailSender mailSender;
/**
* 注入 異常處理bean
*/
@Bean(initMethod = "start")
public ExceptionNoticeHandler nticeHandler(ExceptionNoticeProperties properties) {
List<INoticeProcessor> noticeProcessors = new ArrayList<>(2);
INoticeProcessor noticeProcessor;
DingTalkProperties dingTalkProperties = properties.getDingTalk();
if (null != dingTalkProperties) {
noticeProcessor = new DingTalkNoticeProcessor(restTemplate, dingTalkProperties);
noticeProcessors.add(noticeProcessor);
}
WeChatProperties weChatProperties = properties.getWeChat();
if (null != weChatProperties) {
noticeProcessor = new WeChatNoticeProcessor(restTemplate, weChatProperties);
noticeProcessors.add(noticeProcessor);
}
MailProperties email = properties.getMail();
if (null != email && null != mailSender) {
noticeProcessor = new MailNoticeProcessor(mailSender, email);
noticeProcessors.add(noticeProcessor);
}
Assert.isTrue(noticeProcessors.size() != 0, "Exception notification configuration is incorrect");
return new ExceptionNoticeHandler(properties, noticeProcessors);
}
/**
* 注入異常捕獲aop
*/
@Bean
@ConditionalOnClass(ExceptionNoticeHandler.class)
public ExceptionListener exceptionListener(ExceptionNoticeHandler nticeHandler) {
return new ExceptionListener(nticeHandler);
}
}
4、異常資訊推送處理類
@Slf4j
public class ExceptionNoticeHandler {
private final String SEPARATOR = System.getProperty("line.separator");
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private final BlockingQueue<ExceptionInfo> exceptionInfoBlockingDeque = new ArrayBlockingQueue<>(1024);
private final ExceptionNoticeProperties exceptionProperties;
private final List<INoticeProcessor> noticeProcessors;
public ExceptionNoticeHandler(ExceptionNoticeProperties exceptionProperties,
List<INoticeProcessor> noticeProcessors) {
this.exceptionProperties = exceptionProperties;
this.noticeProcessors = noticeProcessors;
}
/**
* 將捕獲到的異常資訊封裝好之後傳送到阻塞佇列
*/
public Boolean createNotice(Exception ex, JoinPoint joinPoint) {
//校驗當前異常是否是需要 排除的需要統計的異常
if (containsException(ex)) {
return null;
}
log.error("捕獲到異常開始傳送訊息通知:{}method:{}--->", SEPARATOR, joinPoint.getSignature().getName());
//獲取請求引數
Object parameter = getParameter(joinPoint);
//獲取當前請求物件
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String address = null;
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
//獲取請求地址
address = request.getRequestURL().toString() + ((request.getQueryString() != null && request.getQueryString().length() > 0) ? "?" + request.getQueryString() : "");
}
ExceptionInfo exceptionInfo = new ExceptionInfo(ex, joinPoint.getSignature().getName(), exceptionProperties.getIncludedTracePackage(), parameter, address);
exceptionInfo.setProject(exceptionProperties.getProjectName());
return exceptionInfoBlockingDeque.offer(exceptionInfo);
}
/**
* 啟動定時任務傳送異常通知
*/
public void start() {
executor.scheduleAtFixedRate(() -> {
ExceptionInfo exceptionInfo = exceptionInfoBlockingDeque.poll();
if (null != exceptionInfo) {
noticeProcessors.forEach(noticeProcessor -> noticeProcessor.sendNotice(exceptionInfo));
}
}, 6, exceptionProperties.getPeriod(), TimeUnit.SECONDS);
}
/**
* 排除的需要統計的異常
*/
private boolean containsException(Exception exception) {
Class<? extends Exception> exceptionClass = exception.getClass();
List<Class<? extends Exception>> list = exceptionProperties.getExcludeExceptions();
for (Class<? extends Exception> clazz : list) {
//校驗是否存在
if (clazz.isAssignableFrom(exceptionClass)) {
return true;
}
}
return false;
}
/**
* 根據方法和傳入的引數獲取請求引數
* 注意這裡就需要在引數前面加對應的RequestBody 和 RequestParam 註解
*/
private Object getParameter(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
Parameter[] parameters = method.getParameters();
Object[] args = joinPoint.getArgs();
List<Object> argList = new ArrayList<>(parameters.length);
for (int i = 0; i < parameters.length; i++) {
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>(1);
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}
二、如何配置
1、釘釘配置
第一步:建立釘釘群 並在群中新增自定義機器人
對於不太瞭解釘釘機器人配置的同學可以參考:釘釘機器人
具體的也可以參考這篇部落格: 釘釘機器人SDK 封裝預警訊息傳送工具
第二步:增加配置檔案
以下以yml配置檔案的配置方式為例
exception:
notice:
enable: 啟用開關 false或不配置的話本專案不會生效
projectName: 指定異常資訊中的專案名,不填的話預設取 spring.application.name的值
included-trace-package: 追蹤資訊的包含的包名,配置之後只通知此包下的異常資訊
period: 異常資訊傳送的時間週期 以秒為單位 預設值5,異常資訊通知並不是立即傳送的,預設設定了5s的週期
exclude-exceptions:
- 需要排除的異常通知,注意 這裡是異常類的全路徑,可多選
## 釘釘配置
ding-talk:
web-hook: 釘釘機器人的webHook地址,可依次點選釘釘軟體的頭像,機器人管理,選中機器人來檢視
at-mobiles:
- 釘釘機器人傳送通知時 需要@的釘釘使用者賬戶,可多選
msg-type: 訊息文字型別 目前支援 text markdown
2、企業微信配置
第一步:建立企業微信群 並在群中新增自定義機器人
對於不太瞭解企業微信機器人配置的同學可以參考:企業微信機器人
第二步:增加配置檔案
以下以yml配置檔案的配置方式為例
exception:
notice:
enable: 啟用開關 false或不配置的話本專案不會生效
projectName: 指定異常資訊中的專案名,不填的話預設取 spring.application.name的值
included-trace-package: 追蹤資訊的包含的包名,配置之後只通知此包下的異常資訊
period: 異常資訊傳送的時間週期 以秒為單位 預設值5,異常資訊通知並不是立即傳送的,預設設定了5s的週期
exclude-exceptions:
- 需要排除的異常通知,注意 這裡是異常類的全路徑,可多選
## 企業微信配置
we-chat:
web-hook: 企業微信webhook地址
at-phones: 手機號列表,提醒手機號對應的群成員(@某個成員),@all表示提醒所有人 當msg-type=text時才會生效
at-user-ids: userid的列表,提醒群中的指定成員(@某個成員),@all表示提醒所有人 當msg-type=text時才會生效
msg-type: 訊息格式 企業微信支援 (text)、markdown(markdown)、圖片(image)、圖文(news)四種訊息型別 本專案中有 text和markdown兩種可選
3、郵箱配置
這裡以qq郵箱為例
第一步:專案中引入郵箱相關依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
第二步:增加配置檔案
exception:
notice:
enable: 啟用開關 false或不配置的話本專案不會生效
projectName: 指定異常資訊中的專案名,不填的話預設取 spring.application.name的值
included-trace-package: 追蹤資訊的包含的包名,配置之後只通知此包下的異常資訊
period: 異常資訊傳送的時間週期 以秒為單位 預設值5,異常資訊通知並不是立即傳送的,預設設定了5s的週期,主要為了防止異常過多通知刷屏
exclude-exceptions:
- 需要排除的異常通知,注意 這裡是異常類的全路徑,可多選
## 郵箱配置
mail:
from: 傳送人地址
to: 接收人地址
cc: 抄送人地址
spring:
mail:
host: smtp.qq.com 郵箱server地址
username: 4545545@qq.com server端傳送人郵箱地址
password: 郵箱授權碼
郵箱授權碼可以按以下方法獲取
開啟QQ郵箱網頁→設定→賬戶→POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服務→開啟POP3/SMTP服務,然後就能看到授權碼了
注意
:釘釘,企業微信和郵箱配置支援單獨和同時啟用
配置好了配置檔案,接下來可以寫個例子測試一下了
三、測試
我這裡只演示釘釘預警測試,郵箱和微信企業就不再演示了。
介面
@RestController
public class TestController {
@RequestMapping(value = "/queryUser")
public void queryUser(@RequestParam("userId") String userId) throws IllegalAccessException {
throw new IllegalAccessException("監控報警: 使用者不存在id=" + userId);
}
}
配置類
exception:
notice:
enable: true
## 釘釘配置
ding-talk:
web-hook: https://oapi.dingtalk.com/robot/send?access_token=881bada83653fa8af8e08dcd18a9fb403c55b1dbfe5bf239b6f72fdf8e17d5c5
# 釘釘機器人傳送通知時 需要@的釘釘使用者賬戶,可多選
at-mobiles: 15990000149
msg-type: text
介面請求
localhost:8084/queryUser?userId=1212
釘釘預警
git地址
spring-boot-exception-notice
注意: 本工具僅支援整合在springboot+mvc專案中,同時需要jdk版本1.8+
感謝
本專案基本上是基於該專案的,所以非常感謝,不用自己從頭造輪子: exception-notice-spring-boot-starter