添加全局异常消息通知、实现钉钉发送异常消息通知

This commit is contained in:
b2baccline
2020-06-12 19:22:58 +08:00
parent d09f7fa84d
commit 77f24e1496
12 changed files with 396 additions and 72 deletions

View File

@@ -16,6 +16,11 @@
<groupId>com.hccake</groupId>
<artifactId>ballcat-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.hccake</groupId>
<artifactId>ballcat-extend-ding-talk</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
@@ -33,6 +38,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

View File

@@ -0,0 +1,33 @@
package com.hccake.ballcat.common.conf.config;
import com.hccake.ballcat.common.conf.exception.ExceptionHandleTypeEnum;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* @author lingting 2020/6/12 0:15
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "ballcat.exception")
public class ExceptionHandleConfig {
/**
* 处理类型
*/
private ExceptionHandleTypeEnum type = ExceptionHandleTypeEnum.NONE;
/**
* 通知间隔时间 单位秒 默认 5分钟
*/
private long time = TimeUnit.MINUTES.toSeconds(5);
/**
* 消息阈值 即便间隔时间没有到达设定的时间, 但是异常发生的数量达到阈值 则立即发送消息
*/
private long max = 5;
/**
* 堆栈转string 的长度
*/
private int length = 3000;
}

View File

@@ -1,8 +1,14 @@
package com.hccake.ballcat.common.conf.exception;
import com.hccake.ballcat.common.core.exception.handler.DefaultGlobalExceptionHandler;
import com.hccake.ballcat.common.conf.config.ExceptionHandleConfig;
import com.hccake.ballcat.common.conf.exception.handler.DefaultGlobalExceptionHandler;
import com.hccake.ballcat.common.conf.exception.handler.DingTalkGlobalExceptionHandler;
import com.hccake.ballcat.common.core.exception.handler.GlobalExceptionHandler;
import com.hccake.extend.ding.talk.DingTalkSender;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@@ -13,27 +19,45 @@ import org.springframework.scheduling.annotation.EnableAsync;
* @date 2019/10/15 18:20
*/
@EnableAsync
@Configuration
@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class ExceptionHandleAutoConfiguration {
@Value("${spring.application.name}")
private String applicationName;
/**
* 默认的日志处理器
*
* @return DefaultExceptionHandler
*/
@Bean
@ConditionalOnMissingBean(GlobalExceptionHandler.class)
public GlobalExceptionHandler globalExceptionHandler(){
@ConditionalOnProperty(prefix = "ballcat.exception", name = "type", havingValue = "NONE")
public GlobalExceptionHandler defaultGlobalExceptionHandler() {
return new DefaultGlobalExceptionHandler();
}
/**
* 钉钉消息通知的日志处理器
*
* @author lingting 2020-06-12 00:32:40
*/
@Bean
@ConditionalOnMissingBean(GlobalExceptionHandler.class)
@ConditionalOnProperty(prefix = "ballcat.exception", name = "type", havingValue = "DING_TALK")
public GlobalExceptionHandler dingTalkGlobalExceptionHandler(ExceptionHandleConfig exceptionHandleConfig, DingTalkSender sender) {
return new DingTalkGlobalExceptionHandler(exceptionHandleConfig, sender, applicationName);
}
/**
* 默认的日志处理器
*
* @return DefaultExceptionHandler
*/
@Bean
@ConditionalOnMissingBean(GlobalExceptionHandlerResolver.class)
public GlobalExceptionHandlerResolver globalExceptionHandlerResolver(GlobalExceptionHandler globalExceptionHandler){
public GlobalExceptionHandlerResolver globalExceptionHandlerResolver(GlobalExceptionHandler globalExceptionHandler) {
return new GlobalExceptionHandlerResolver(globalExceptionHandler);
}

View File

@@ -0,0 +1,22 @@
package com.hccake.ballcat.common.conf.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 异常处理类型
*
* @author lingting 2020/6/12 0:18
*/
@Getter
@AllArgsConstructor
public enum ExceptionHandleTypeEnum {
/**
* 异常处理通知类型 说明
*/
NONE("不通知"),
DING_TALK("通过钉钉通知"),
MAIL("邮件通知"),
;
private final String text;
}

View File

@@ -1,7 +1,7 @@
package com.hccake.ballcat.common.conf.exception;
import com.hccake.ballcat.common.core.exception.handler.GlobalExceptionHandler;
import com.hccake.ballcat.common.core.exception.BusinessException;
import com.hccake.ballcat.common.core.exception.handler.GlobalExceptionHandler;
import com.hccake.ballcat.common.core.result.R;
import com.hccake.ballcat.common.core.result.SystemResultCode;
import lombok.RequiredArgsConstructor;
@@ -21,6 +21,7 @@ import java.util.List;
/**
* 全局异常处理
*
* @author Hccake
*/
@Slf4j
@@ -31,12 +32,13 @@ public class GlobalExceptionHandlerResolver {
/**
* 全局异常捕获
*
* @param e the e
* @return R
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleGlobalException(Exception e) {
public R<String> handleGlobalException(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
globalExceptionHandler.handle(e);
return R.failed(SystemResultCode.SERVER_ERROR, e.getLocalizedMessage());
@@ -47,12 +49,13 @@ public class GlobalExceptionHandlerResolver {
* 自定义业务异常捕获
* 业务异常响应码推荐使用200
* 用 result 结构中的code做为业务错误码标识
*
* @param e the e
* @return R
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public R handleBallCatException(BusinessException e) {
public R<String> handleBallCatException(BusinessException e) {
log.error("自定义异常信息 ex={}", e.getMessage(), e);
globalExceptionHandler.handle(e);
return R.failed(e.getCode(), e.getMsg());
@@ -67,7 +70,7 @@ public class GlobalExceptionHandlerResolver {
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R handleAccessDeniedException(AccessDeniedException e) {
public R<String> handleAccessDeniedException(AccessDeniedException e) {
String msg = SpringSecurityMessageSource.getAccessor()
.getMessage("AbstractAccessDecisionManager.accessDenied"
, e.getMessage());
@@ -84,13 +87,13 @@ public class GlobalExceptionHandlerResolver {
*/
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleBodyValidException(Exception exception) {
public R<String> handleBodyValidException(Exception exception) {
List<FieldError> fieldErrors;
if(exception instanceof BindException){
BindException bindException = (BindException)exception;
if (exception instanceof BindException) {
BindException bindException = (BindException) exception;
fieldErrors = bindException.getBindingResult().getFieldErrors();
}else{
MethodArgumentNotValidException e = (MethodArgumentNotValidException)exception;
} else {
MethodArgumentNotValidException e = (MethodArgumentNotValidException) exception;
fieldErrors = e.getBindingResult().getFieldErrors();
}
@@ -105,17 +108,17 @@ public class GlobalExceptionHandlerResolver {
/**
* 单体参数校验异常
* validation Exception
*
* @param e the e
* @return R
*/
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleValidationException(Exception e) {
public R<String> handleValidationException(Exception e) {
log.error("参数绑定异常 ex={}", e.getMessage(), e);
globalExceptionHandler.handle(e);
return R.failed(SystemResultCode.BAD_REQUEST, e.getLocalizedMessage());
}
}

View File

@@ -0,0 +1,54 @@
package com.hccake.ballcat.common.conf.exception.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
/**
* 异常通知消息
*
* @author lingting 2020/6/12 16:07
*/
@Getter
@Setter
@Accessors(chain = true)
public class ExceptionMessage {
/**
* 消息
*/
private String message;
/**
* 数量
*/
private int number;
/**
* 堆栈
*/
private String stack;
/**
* 最新的触发时间
*/
private String time;
/**
* 机器地址
*/
private String mac;
/**
* 线程id
*/
private long threadId;
/**
* 服务名
*/
private String applicationName;
@Override
public String toString() {
return "服务名称:" + applicationName
+ "\n机器地址" + mac
+ "\n触发时间" + time
+ "\n线程id" + threadId
+ "\n数量" + number
+ "\n堆栈" + stack;
}
}

View File

@@ -0,0 +1,24 @@
package com.hccake.ballcat.common.conf.exception.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
/**
* 异常消息通知响应
*
* @author lingting 2020/6/12 19:07
*/
@Getter
@Setter
@Accessors(chain = true)
public class ExceptionNoticeResponse {
/**
* 是否成功
*/
private boolean success;
/**
* 错误信息
*/
private String errMsg;
}

View File

@@ -0,0 +1,117 @@
package com.hccake.ballcat.common.conf.exception.handler;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.exceptions.ExceptionUtil;
import com.hccake.ballcat.common.conf.config.ExceptionHandleConfig;
import com.hccake.ballcat.common.conf.exception.domain.ExceptionMessage;
import com.hccake.ballcat.common.conf.exception.domain.ExceptionNoticeResponse;
import com.hccake.ballcat.common.core.exception.handler.GlobalExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 消息通知顶层类
*
* @author lingting 2020/6/12 0:35
*/
@Slf4j
public abstract class AbstractNoticeGlobalExceptionHandler extends Thread implements GlobalExceptionHandler {
private final ExceptionHandleConfig config;
/**
* 通知消息存放 e.message 堆栈信息
*/
private Map<String, ExceptionMessage> messages = new ConcurrentHashMap<>(10);
/**
* 异常发生数
*/
private long number = 0;
/**
* 用来当做锁
*/
private final String lock = "";
/**
* 本地物理地址
*/
private String mac;
private final String applicationName;
public AbstractNoticeGlobalExceptionHandler(ExceptionHandleConfig config, String applicationName) {
this.config = config;
this.applicationName = applicationName;
try {
byte[] mac = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));
}
this.mac = sb.toString();
} catch (Exception e) {
mac = "获取失败!";
}
this.start();
}
@Override
public void handle(Throwable e) {
synchronized (lock) {
number++;
ExceptionMessage message = messages.getOrDefault(
e.getMessage(),
new ExceptionMessage().setNumber(0).setMac(mac).setApplicationName(applicationName)
);
message.setNumber(message.getNumber() + 1)
.setStack(ExceptionUtil.stacktraceToString(e, config.getLength()).replaceAll("\\r", ""))
.setTime(DateUtil.now())
.setThreadId(Thread.currentThread().getId());
messages.put(e.getMessage(), message);
}
}
@Override
public void run() {
this.setName("exception-notice-thread-" + config.getType().name());
log.debug("异常消息通知线程启动!");
TimeInterval interval = new TimeInterval();
while (true) {
try {
if (interval.intervalSecond() >= config.getTime() || number >= config.getMax()) {
if (messages.size() == 0) {
interval.restart();
continue;
}
Map<String, ExceptionMessage> sendMessages;
synchronized (lock) {
sendMessages = messages;
messages = new ConcurrentHashMap<>(10);
number = 0;
interval.restart();
}
sendMessages.forEach((k, v) -> {
ExceptionNoticeResponse response = send(v);
if (!response.isSuccess()) {
log.error("消息通知发送失败! msg: {}", response.getErrMsg());
}
});
}
} catch (Exception e) {
log.error("消息通知异常!", e);
}
}
}
/**
* 发送通知
*
* @param sendMessage 发送的消息
* @return 返回消息发送状态,如果发送失败需要设置失败信息
* @author lingting 2020-06-12 00:37:23
*/
public abstract ExceptionNoticeResponse send(ExceptionMessage sendMessage);
}

View File

@@ -0,0 +1,21 @@
package com.hccake.ballcat.common.conf.exception.handler;
import com.hccake.ballcat.common.core.exception.handler.GlobalExceptionHandler;
/**
* @author Hccake
* @version 1.0
* @date 2019/10/18 17:06
* 默认的异常日志处理类
*/
public class DefaultGlobalExceptionHandler implements GlobalExceptionHandler {
/**
* 在此处理日志
* 默认什么都不处理
*
* @param throwable 异常信息
*/
@Override
public void handle(Throwable throwable) {
}
}

View File

@@ -0,0 +1,32 @@
package com.hccake.ballcat.common.conf.exception.handler;
import com.hccake.ballcat.common.conf.config.ExceptionHandleConfig;
import com.hccake.ballcat.common.conf.exception.domain.ExceptionMessage;
import com.hccake.ballcat.common.conf.exception.domain.ExceptionNoticeResponse;
import com.hccake.extend.ding.talk.DingTalkResponse;
import com.hccake.extend.ding.talk.DingTalkSender;
import com.hccake.extend.ding.talk.message.DingTalkTextMessage;
import lombok.extern.slf4j.Slf4j;
/**
* 钉钉消息通知
*
* @author lingting 2020/6/12 0:25
*/
@Slf4j
public class DingTalkGlobalExceptionHandler extends AbstractNoticeGlobalExceptionHandler {
private final DingTalkSender sender;
public DingTalkGlobalExceptionHandler(ExceptionHandleConfig config, DingTalkSender sender, String applicationName) {
super(config, applicationName);
this.sender = sender;
}
@Override
public ExceptionNoticeResponse send(ExceptionMessage sendMessage) {
DingTalkResponse response = sender.sendMessage(new DingTalkTextMessage().setContent(sendMessage.toString()));
return new ExceptionNoticeResponse()
.setErrMsg(response.getResponse())
.setSuccess(response.isSuccess());
}
}

View File

@@ -3,5 +3,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hccake.ballcat.common.conf.config.JacksonConfig,\
com.hccake.ballcat.common.conf.exception.ExceptionHandleAutoConfiguration,\
com.hccake.ballcat.common.conf.mybatis.MybatisPlusConfig,\
com.hccake.ballcat.common.conf.web.WebMvcConfig
com.hccake.ballcat.common.conf.web.WebMvcConfig,\
com.hccake.ballcat.common.conf.config.ExceptionHandleConfig

View File

@@ -1,17 +0,0 @@
package com.hccake.ballcat.common.core.exception.handler;
/**
* @author Hccake
* @version 1.0
* @date 2019/10/18 17:06
* 默认的异常日志处理类
*/
public class DefaultGlobalExceptionHandler implements GlobalExceptionHandler {
/**
* 在此处理日志
* 默认什么都不处理
* @param throwable 异常信息
*/
@Override
public void handle(Throwable throwable) {}
}