不建议在框架层面进行异常的自动、统一处理,尤其不要随意捕获异常。但是,框架可以做兜底工作。如果异常上升到最上层逻辑还是无法处理的话,可以以统一的方式进行异常转换,比如通过 @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); } }); |