对于Java理论在《Java异常处理》[1]中已经阐述了,看看理论如何指导落地
现流行的文章SpringBoot如何优雅处理异常,落地的确方便,使用AOP统一处理异常,但只是处理了api层次的异常
应用中抛出异常有两种方式:
1.带有ErrorCode的异常2.明确类型的异常
对于controller层,也是面向用户的,需要error code,所以采用第一种方式
前端通过映射关系给出更好用户体验的提示语,也有很多项目都是controller层直接拼接出提示语,前端直接展示
所以一般会定义一个接口ErrorCode
interface ErrorCode { String getErrorCode(); String getMessage(); }
具体的实现可以通过enum
@Getter enum ApiErrorCode implements ErrorCode { USER_NOT_FOUND("10000","用户不存在"); private String errorCode; private String message; }
再定义一个统一异常
public class ApiException extends RuntimeException { public ApiException(ErrorCode errorCode) { } }
在aop拦截时,直接拦截此异常就行
api层次的异常可以这么处理,那业务层呢?很多时候都是缺失设计的,这也在上篇说过exception从语法层面看很简单,但要设计一个好的异常是很难的
大多项目直接把api exception拿来当做business exception使用
对于一个良好的业务接口,应该采用第二种方法:细致的异常
User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;
已检查异常要比错误返回码(许多老式的语言中使用)好很多。迟早(或许不久),人们将不能检查一个错误返回值;使用编译程序来实施正确的错误处理是一件好事。同参数与返回值一样,这样的已检查异常对一个对象的API来说是整体的一个不可分割部分
这样的接口更丰富,也更面向对象,可也给客户端带来的麻烦,缺点在上篇已经阐述
对可恢复的情况使用已检查异常,对程序错误使用运行时异常
在大多项目中,其实业务层抛出异常后,通常会“可恢复”吗?大多数情况也需要用户手工干预,系统无法自行恢复,比如UserNotFoundException, PasswordNotMatchException系统能怎么处理,无非还是得给用户重新输入用户名和密码
在controller层去调用service方法,也只能如此处理
Response login(String username,String password) { try { userService.login(username,password); }catch(UserNotFoundException ue) { throw new ApiException(ApiErrorCode.USER_NOT_FOUND); }catch(PasswordNotMatchException pe){ throw new ApiException(ApiErrorCode.Password_Not_Match); } }
这样处理也就理论化了,带来了多少优点呢?这些缺点不正是checked exception被嘟囔的地方吗
那我们把业务异常也定义为runtime exception,这样减少客户端压力,想处理就处理,不想处理,我们也可在拦截器中兜底
不过抛出运行期异常,减少客户端的压力,但也带来了接口不明确的困惑
非检查型异常的最大风险之一就是它并没有按照检查型异常采用的方式那样自我文档化。除非 API 的创建者明确地文档化将要抛出的异常,否则调用者没有办法知道在他们的代码中将要捕获的异常是什么
总结起来,还是那句话,异常语法很简单,但设计好异常不易;现在技术快速发展,通过技术手段可以达到更大的便捷性,但不能只有技术手段而忽略设计,没有设计的代码称不上好代码,可以取舍,但不能全舍