方案介绍
将异常信息放在日志里面,如果磁盘定期清理,会导致很久之前的日志丢失,因此考虑将日志中的异常信息存在表里,方便后期查看定位问题。
由于项目是基于SpringBoot构架的,所以采用@AdviceController+@ExceptionHandler对全局异常进行拦截和处理,而后将异常信息通过异步任务的方式记录到数据库,之所以采用异步任务,是防止异常记录出现问题影响主流程:
方案实现
定义异常处理表
CREATE TABLE exception_log_t (
id int(10) NOT NULL AUTO_INCREMENT COMMENT '主键id',
msg varchar(1024) NOT NULL COMMENT '异常信息',
stack_trace text DEFAULT NULL COMMENT '异常堆栈信息',
create_by bigint(10) DEFAULT NULL COMMENT '创建人',
creation_date datetime NOT NULL COMMENT '异常发生时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='异常信息日志表';
GlobalExceptionHandler带有@ControllerAdvice和@ExceptionHandler注解,可以拦截异常并处理,同时组装异常记录信息给异步任务进行记录
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理自定义异常
*/
@ExceptionHandler(value = CommonException.class)
@ResponseBody
public BasicResponse bizExceptionHandler(CommonException e) {
log.error("CommonException error info:", e);
recordExceptionMsg(e);
return BasicResponse.commonError(e);
}
/**
* 处理其他异常
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public BasicResponse exceptionHandler(Exception e) {
log.error("Exception error info:", e);
recordExceptionMsg(e);
return BasicResponse.errorWithMsg(e.getMessage());
}
/**
* 处理自定义异常
*/
@ExceptionHandler(value = IllegalStateException.class)
@ResponseBody
public BasicResponse IllegalStateExceptionHandler(IllegalStateException e) {
log.error("IllegalStateException error info:", e);
recordExceptionMsg(e);
return BasicResponse.errorWithMsg(e.getMessage());
}
/**
* 处理NoSuchAlgorithmException异常
*/
@ExceptionHandler(value = NoSuchAlgorithmException.class)
@ResponseBody
public BasicResponse NoSuchAlgorithmExceptionHandler(NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException error info:", e);
recordExceptionMsg(e);
return BasicResponse.errorWithMsg(e.getMessage());
}
/**
* 组装异常记录信息
*/
private <T extends Exception> void recordExceptionMsg(T ex) {
String exStackTrace = "";
try {
exStackTrace = getExStackTrace(ex);
} catch (IOException e) {
log.error("get exception stack track info error:", e);
}
String message = ex.getMessage();
if (message.length() > 1024) {
message = message.substring(0, 1024);
}
ExceptionMsgPo exceptionMsgPo = ExceptionMsgPo.builder()
.msg(message)
.stackTrace(exStackTrace)
.creationDate(new Date())
.createBy(UserContext.getUserId())
.build();
AsyncRecordExceptionMsg asyncRecordExMsg = AppContextUtil.getBean(AsyncRecordExceptionMsg.class);
// 调用异步任务入库
asyncRecordExMsg.recordExceptionMsgTask(exceptionMsgPo);
}
private <T extends Exception> String getExStackTrace(T ex) throws IOException {
//读取异常堆栈信息
ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
ex.printStackTrace(new PrintStream(arrayOutputStream));
//通过字节数组转换输入输出流
BufferedReader fr = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(arrayOutputStream.toByteArray())));
String str;
StringBuilder exceptionSb = new StringBuilder();
while ((str = fr.readLine()) != null) {
exceptionSb.append(str);
exceptionSb.append("\n");
}
return exceptionSb.toString();
}
}
异步任务记录异常比较简单, 就调用IExceptionMsgMapper进行入库
@Component
@Slf4j
public class AsyncRecordExceptionMsg {
@Autowired
private IExceptionMsgMapper exceptionMsgMapper;
@Async("asyncPoolTaskExecutor")
public void recordExceptionMsgTask(ExceptionMsgPo exceptionMsgPo){
log.info("begin to do recordExceptionMsgTask");
exceptionMsgMapper.insert(exceptionMsgPo);
log.info("end of recordExceptionMsgTask");
}
}
需要注意的是,@Async异步任务虽然方便,但是要注意控制线程数量,避免线程耗尽资源, @Async("asyncPoolTaskExecutor")
中的asyncPoolTaskExecutor将线程池定义如下:
@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
taskExecutor.setCorePoolSize(10);
//线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(100);
//缓存队列
taskExecutor.setQueueCapacity(50);
//许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
//异步方法内部线程名称
taskExecutor.setThreadNamePrefix("async-task-");
/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
ExceptionMsgPo定义
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("exception_log_t")
public class ExceptionMsgPo {
@TableId(value="id",type = IdType.AUTO)
private Long id;
@TableField("msg")
private String msg;
@TableField("stack_trace")
private String stackTrace;
@TableField("create_by")
protected Long createBy;
@TableField("creation_date")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date creationDate;
}
测试效果
应用抛出异常,记录