2021-11-25 异常处理

不建议在框架层面进行异常的自动、统一处理,尤其不要随意捕获异常。但是,框架可以做兜底工作。如果异常上升到最上层逻辑还是无法处理的话,可以以统一的方式进行异常转换,比如通过 @RestControllerAdvice + @ExceptionHandler,来捕获这些“未处理”异常。

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

        }

    }

}

防止丢失异类型、堆栈信息等:

catch (IOException e) {

    log.error("文件读取错误", e);

    throw new RuntimeException("系统忙请稍后再试");

}

catch (IOException e) {

    throw new RuntimeException("系统忙请稍后再试", e);

}

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

finally 代码块自己负责异常捕获和处理,或者可以把 try 中的异常作为主异常抛出,使用 addSuppressed 方法把 finally 中的异常附加到主异常上:

@GetMapping("right")

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);

        }

    }

}

@GetMapping("right2")

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;

}

启用与释放资源类的异常建议使用try-with-resources模式:

@GetMapping("useresourceright")

public void useresourceright() throws Exception {

    try (TestResource testResource = new TestResource()){

        testResource.read();

    }

}

别把异常定义为静态变量,把异常定义为静态变量会导致异常信息固化,这就和异常的栈一定是需要根据当前调用来动态获取相矛盾。解决方法为,通过不同的方法把每一种异常都 new 出来抛出即可。

确保正确处理了线程池中任务的异常,如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;如果任务通过 submit 提交意味着我们关心任务的执行结果,应该通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。

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 方法提交任务到线程池处理,如果出现异常会导致线程退出,控制台输出中可以看到异常信息。那么,把 execute 方法改为 submit,线程还会退出吗,异常还能被处理程序捕获到吗?修改代码后重新执行程序可以看到如下日志,说明线程没退出,异常也没记录被生吞了。——Future源码

修改后的代码如下所示,我们把 submit 返回的 Future 放到了 List 中,随后遍历 List 来捕获所有任务的异常。这么做确实合乎情理。既然是以 submit 方式来提交任务,那么我们应该关心任务的执行结果,否则应该以 execute 来提交任务:

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);

    }

});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值