异常信息记录入库

方案介绍

将异常信息放在日志里面,如果磁盘定期清理,会导致很久之前的日志丢失,因此考虑将日志中的异常信息存在表里,方便后期查看定位问题。
由于项目是基于SpringBoot构架的,所以采用@AdviceController+@ExceptionHandler对全局异常进行拦截和处理,而后将异常信息通过异步任务的方式记录到数据库,之所以采用异步任务,是防止异常记录出现问题影响主流程:

@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;
}

测试效果

应用抛出异常,记录
在这里插入图片描述

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值