java-基础-正确处理异常

1. 捕获和处理异常容易犯的错

每层架构的工作性质不同,且从业务性质上异常可能分为业务异常和系统异常两大类,这就决定了很难进行统一的异常处理。

我们从底向上看一下三层架构:

Repository 层出现异常或许可以忽略,或许可以降级,或许需要转化为一个友好的异常。如果一律捕获异常仅记录日志,很可能业务逻辑已经出错,而用户和程序本身完全感知不到。

Service 层往往涉及数据库事务,出现异常同样不适合捕获,否则事务无法自动回滚。此外 Service 层涉及业务逻辑,有些业务逻辑执行中遇到业务异常,可能需要在异常后转入分支业务流程。如果业务异常都被框架捕获了,业务功能就会不正常。

如果下层异常上升到 Controller 层还是无法处理的话,Controller 层往往会给予用户友好提示,或是根据每一个 API 的异常表返回指定的异常类型,同样无法对所有异常一视同仁。

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

对于自定义的业务异常,以 Warn 级别的日志记录异常以及当前 URL、执行方法等信息后,提取异常中的错误码和消息等信息,转换为合适的 API 包装体返回给 API 调用方;

对于无法处理的系统异常,以 Error 级别的日志记录异常和上下文信息(比如 URL、参数、用户 ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以 API 包装体返回给调用方

@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.getRequestU
		return new APIResponse(false, null, exception.getCode(), exception
    } else {
            log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequest
            return new APIResponse(false, null, GENERIC_SERVER_ERROR_C
        }
}                                                                                             18 }

出现运行时系统异常后,异常处理程序会直接把异常转换为 JSON 返回给调用方

要做得更好,你可以把相关出入参、用户信息在脱敏后记录到日志中,方便出现问题时根据上下文进一步排查。

2. 捕获了异常后直接生吞

第二个错,捕获了异常后直接生吞。在任何时候,我们捕获了异常都不应该生吞,也就是直接丢弃异常不记录、不抛出。这样的处理方式还不如不捕获异常,因为被生吞掉的异常一旦 导致 Bug,就很难在程序中找到蛛丝马迹,使得 Bug 排查工作难上加难。

通常情况下,生吞异常的原因,可能是不希望自己的方法抛出受检异常,只是为了把异常“处理掉”而捕获并生吞异常,也可能是想当然地认为异常并不重要或不可能产生。但不管是什么原因,不管是你认为多么不重要的异常,都不应该生吞,哪怕是一个日志也好。

3. 丢弃异常的原始信息

正确处理

 catch (IOException e) {
 	log.error("文件读取错误", e);
	throw new RuntimeException("系统忙请稍后再试");
}

或者,把原始异常作为转换后新异常的 cause,原始异常信息同样不会丢:

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

4. 小心 finally 中的异常

@GetMapping("wrong")
public void wrong() {
 try {
 	log.info("try");
 	//异常丢失
 	throw new RuntimeException("try");
 	} finally {
		log.info("finally");
		throw new RuntimeException("finally");
 	}
 }

最后在日志中只能看到 finally 中的异常,虽然 try 中的逻辑出现了异常,但却被 finally 中的异常覆盖了。这是非常危险的,特别是 finally 中出现的异常是偶发的,就会在部分时候覆盖 try 中的异常,让问题更不明显:

13:34:42.247] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherSe

2 java.lang.RuntimeException: finally

至于异常为什么被覆盖,原因也很简单,因为一个方法无法出现两个异常。修复方式是,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);
	 }
 }	
}

或者可以把 try 中的异常作为主异常抛出,使用 addSuppressed 方法把 finally 中的异常附加到主异常上:

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

运行方法可以得到如下异常信息,其中同时包含了主异常和被屏蔽的异常:java.lang.RuntimeException: try

at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueContro

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

...

Suppressed: java.lang.RuntimeException: finally

at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueCont

... 54 common frames omitted

5. 对于IO

try-with-resources 可以保留异常信息

6. 千万别把异常定义为静态变量

7. 提交线程池的任务出了异常会怎么样?

1. 以 execute 方法提交到线程池的异步任务,最好在任务内部做好异常处理;

2. 设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序:

new ThreadFactoryBuilder()
.setNameFormat(prefix+"%d")
.setUncaughtExceptionHandler((thread, throwable)-> log.error("ThreadPool {} g
.get()

或者设置全局的默认未捕获异常处理程序:

static {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable)-> log.error(
}

通过线程池 ExecutorService 的 execute 方法提交任务到线程池处理,如果出现异常会导致线程退出,控制台输出中可以看到异常信息。那么,把 execute 方法改为 submit,线程还会退出吗,异常还能被处理程序捕获到吗?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值