异常处理注意事项

框架层统一异常处理

1. 在框架层注意进行异常的自动、统一处理,做兜底工作;
2. 处理一些异常上升到最上层逻辑还是无法处理,以统一的方式i纪念性异常转换,如@RestControllerAdvice+@ExceptionHandler来捕获‘未处理’异常;
  - 对于自定义的业务异常,以Warn级别的日志记录异常以及当前URL、执行方法等信息后,提取异常中的错误码和消息等信息,转换为合适的API包装体给API调用方;
  - 对于无法处理的系统异常,以Error级别的日志记录异常的上下文信息(比如URL、参数、用户id)后,转换为普适的“服务器忙,请稍后再试”异常信息,统一以API包装体返回给调用方。
//出现运行时系统异常后,异常处理程序会直接把异常转换为JSON返回给调用方:
//可以把相关出入参、用户信息在脱敏后记录到日志中,方便出现问题时根据上下文进一步排查;
@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
    private static int GENERIC_SERVER_ERROR_CODE = 2000;
    private static String GENERIC_SERVER_ERROR_MESSAGE = "服务器忙,请稍后再试";

    @ExceptionHandler
    public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
        if (ex instanceof BusinessException) {
            BusinessException exception = (BusinessException) ex;
            log.warn(String.format("访问 %s -> %s 出现业务异常!", req.getRequestURI(), method.toString()), ex);
            return new APIResponse(false, null, exception.getCode(), exception.getMessage());
        } else {
            log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequestURI(), method.toString()), ex);
            return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
        }
    }
}

捕获异常后避免直接生吞

1. 如果被生吞掉的异常一旦导致Bug,很难排查;
2. 生吞异常的原因:
  - 不希望方法抛出受检异常,只是为了把异常’处理掉‘;
  - 想当然地认为异常不重要或不可能产生等;
3. 这些不管多么不重要的异常,都不应该生吞,哪怕是一个日志也好;

避免丢弃异常的原始信息

1. 异常的信息应包含:原始异常信息,异常消息、异常类型、栈信息
2. 记录完整的异常信息:

catch (IOException e) {
    log.error("文件读取错误", e);
    throw new RuntimeException("系统忙请稍后再试");
}
或把原始异常作为转换后新异常的cause,原始异常信息统一不会丢:
catch (IOException e) {
    throw new RuntimeException("系统忙请稍后再试", e);
}

抛出异常应指定消息

异常处理,除了通过日志正确记录异常原始信息外,通过三种处理模式:
  - 转换,即转换新的异常抛出。最好把新抛出的异常指定分类和明确的异常消息,并最好通过cause关联老异常;
  - 重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试;
  - 恢复,即尝试进行降级处理,或使用默认值来替代原始数据。

finally中异常的处理

  • finally中出现异常会覆盖try中逻辑出现的异常(一个方法无法出现两个异常)

//①finally代码块自己负责异常捕获和处理
public void right() {
    try {
        log.info("try");
        throw new RuntimeException("try");
    } finally {
        log.info("finally");
        try {
            throw new RuntimeException("finally");
        } catch (Exception ex) {
            log.error("finally", ex);
        }
    }
}

//②把try中的异常作为主异常抛出,使用addSuppressed方法把finally中的异常附加到主异常上;
public void right2() throws Exception {
    Exception e = null;
    try {
        log.info("try");
        throw new RuntimeException("try");
    } catch (Exception ex) {
        e = ex;
    } finally {
        log.info("finally");
        try {
            throw new RuntimeException("finally");
        } catch (Exception ex) {
            if (e!= null) {
                e.addSuppressed(ex);
            } else {
                e = ex;
            }
        }
    }
    throw e;
}
//③,
  • finally中也有返回值,会以finally中的为准

//因为finally代码块的原理是复制finally代码块内容,分别放在try-catch代码块所有正常执行路径以及异常执行路径的出口中。
//所以不管是正常还是异常执行,finally都是子厚执行的,所以肯定是finally语句块中为准。

对于实现了AutoCloseable接口的资源

1. 对于实现了AutoCloseable接口的资源,建议使用try-with-resources来释放资源,否则会出现释放资源时异常覆盖主异常的问题;
解决:
//测试资源,read和close方法都会抛出异常
public class TestResource implements AutoCloseable {
    public void read() throws Exception{
        throw new Exception("read error");
    }
    @Override
    public void close() throws Exception {
        throw new Exception("close error");
    }
}
//try-with-resources模式
public void useResourceRight() throws Exception {
    try (TestResource testResource = new TestResource()){
        testResource.read();
    }
}

避免把异常定义为静态变量

异常定义为了静态变量,会导致异常栈信息错乱

提交线程池的任务出现异常

1. 因为异常,线程会退出,线程池只能重新创建一个线程;
解决:
  - ①以execute方法提交到线程池的异步任务,做好在任务内部做好异常处理;
  - ②设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序;

new ThreadFactoryBuilder()
  .setNameFormat(prefix+"%d")
  .setUncaughtExceptionHandler((thread, throwable)-> log.error("ThreadPool {} got exception", thread, throwable))
  .get()
或者设置全局的默认未捕获异常处理程序:

static {
    Thread.setDefaultUncaughtExceptionHandler((thread, throwable)-> log.error("Thread {} got exception", thread, throwable));
}
//通过线程池ExecutorService的execute方法提交任务到线程池处理,如果出现异常会导致线程退出,控制台输出中可以看到异常信息。
2. 把execute方法改为submit,线程没有退出,异常没有记录被生吞;因为异常存到了一个outcom字段中,只有在调用get方法获取FutureTask结果的时候,才会以ExecutionException形式重新抛出异常;
//把submit返回的Future放到List中,随后遍历list来捕获所有异常;
List<Future> tasks = IntStream.rangeClosed(1, 10).mapToObj(i -> threadPool.submit(() -> {
    if (i == 5) throw new RuntimeException("error");
    log.info("I'm done : {}", i);
})).collect(Collectors.toList());

tasks.forEach(task-> {
    try {
        task.get();
    } catch (Exception e) {
        log.error("Got exception", e);
    }
});
3. 确保正确处理了线程池中任务的异常;
4. 任务通过execute提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,因此尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;
5. 任务通过submit提交以为着我们关心任务的执行结果,应该通过拿到的Future调用其get方法来获得任务运行结果和可能出现的异常,否则异常就被生吞;

 原文链接:https://time.geekbang.org/column/article/220230

posted @ 2021-12-15 16:26  白玉神驹  阅读(251)  评论(0)    收藏  举报