前言
- 本随笔是最近进行实际项目开发时总结的一些经验,可供需要的伙伴借鉴。
- 如有不足之处欢迎评论指正。
场景
- 使用java提供服务的后端系统,使用者(可能是web前端或者是第三方调用者)通过api形式进行调用。
以前可能遇到的问题
- 这里会不会发生异常?如果发生了我应该怎么做?
- 我要定义哪些自定义异常?
- 捕获到一个"未知"异常时,我该将其抛出去还是就地打印其堆栈信息?
问题分析
- 为什么会产生异常? 简单理解就是,异常是程序执行过程中未按预想状态执行的状态。
- 为什么要自定义异常? 1)与系统(这里指的是非本项目)异常做区分,这样我们就能争对不同的异常做不同的事情。 2)自定义异常也是一个自定义对象,可以携带一些额外的信息。
- 异常产生之后应该做什么? 1)打印异常堆栈:这种情况下,一般需要运维或开发人员定位问题并人工解决,因为堆栈是打印给人看的,要不然也就不打印了 2)执行候选方案等(可能也需要打印堆栈)
对异常进行分类
- 集合项目和业务逻辑对可能产生的异常做分类。不管系统是干什么的,我们都可以将其分为“系统异常”和“非系统异常”,如果觉得太笼统可以将他们二者进行细分(最后你可能会发现没有细分的必要)。 系统异常: 这类异常通常是大致地提示使用方系统不可用,同时需要相关人员更进处理的 非系统异常:这类异常通常是一些义务逻辑而已,比如检查到用户参数有误后抛出的异常
- 异常分类原则:先明确定义好什么样的系统属于系统异常,什么样的异常属于自定义异常,当捕获到一个异常或产生错误时,看这个异常是哪种场景,然后在归类。
- 对分类异常进行处理。既然我们抛出了异常,那就得对其进行捕获处理,如果你的系统设计很好,那么一般情况是对异常进行统一捕获,这种情况下可能会先捕获我们自定义异常,然后再捕获最大异常,由于自定义异常一般已经携带了相关的错误信息,所以我们很容易控制返回给调用者的,所以要争对最大异常定义一个默认的返回值。
正确对待第三方工具(依赖)抛出的异常
- 经过上面的定义,对于自己代码产生的异常可能有一个较好的解决方案了,但是捕获到别人抛出的异常时,可能就不知所措(刚开始我就是这样),这时候再回去看看刚刚的描述或许就会更加清晰了,如果抛异常的代码不是我们的依赖,而是我们项目的一部分,那么你肯定也能清楚的知道这个异常在我们的系统中属于一个怎样的级别。从关系上看,我们是依赖方,所以被依赖方不可能使用依赖方定义的异常。
- 所以当我们捕获到一个依赖方的异常时,需要判断这个异常级别应该对应于我们系统已经定义的哪个异常,然后转换为我们自己定义的异常即可,这里要注意,如果这个异常是需要打印错误堆栈的,那么我们绝对不能把原来的异常直接扔掉,最好是将其作为嵌套异常包含在我们的自定义异常中再将其抛出,这就相当于给依赖方抛出的异常贴上一个标签,以便我们能进行统一捕获处理。
用有限的异常类处理义务中复杂多变的无限可能
这个问题,其实大家都在使用,那就是配合错误码使用。和定义异常类一样的到了,定义和使用错误码的时候也要将其归类定义,避免重复定义和混乱使用。而且到最后你一定会发现,所定义的错误码大部分是业务错误码居多,即该错误码明确代表了一个错误码描述,而这个错误描述是给调用方看的,而系统级别的异常详细信息已经堆栈形式在日志中打印出来了,那不同的错再多定义一个错误码也意义不大。当然,如果要通过错误码对异常分类进行统计的情况除外,但是如果一个系统已经到了这个地步的话基本可以选择其他方案了。
异常类和错误码定义示例
需求
- 对本系统的异常类型进行抽象并定义相应的异常。
- 自定义错误对象能携带相关的错误信息,以便能统一进行捕获处理
- 需要保证错误码不会混乱使用,即“系统级别”类错误码只能用于“系统”类异常,不能混乱使用
示例代码
public interface ErrorCode { String getErrorCode(); String getErrorDesc();}
/** * 用户异常错误码枚举 */public enum UserErrorCode implements ErrorCode { /** * 用户名错误 */ USERNAME_ERR("username-err", "用户名错误"), /** * 密码错误 */ PASSWORD_ERR("PASSWORD-ERR", "密码错误"), ; UserErrorCode(String errorCode, String errorDesc) { this.errorCode = errorCode; this.errorDesc = errorDesc; } // 错误码. @Getter private String errorCode; // 错误码对应的外部描述信息. @Getter private String errorDesc;}
/** * 系统异常错误码枚举 */public enum SystemErrorCode implements ErrorCode { /** * 系统内部错误 */ SYSTEM_ERR("system-err", "系统内部错误"), ; SystemErrorCode(String errorCode, String errorDesc) { this.errorCode = errorCode; this.errorDesc = errorDesc; } @Getter private String errorCode; @Getter private String errorDesc;}
import lombok.Getter;/** * 这里定义的是用户级别的异常类,只允许通过用户级别的错误码进行构建异常对象 * 其他异常定义同本示例. */public class UserException extends RuntimeException implements ErrorCode { @Getter private String errorCode; @Getter private String errorDesc; /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. */ public UserException(UserErrorCode errorCode) { super(); this.errorCode = errorCode.getErrorCode(); this.errorDesc = errorCode.getErrorDesc(); } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param message 最好是自定义的详细描述信息,用于打日志,也可以和{ @link ErrorCode#getErrorDesc()}相同 */ public UserException(UserErrorCode errorCode, String message) { super(message); this.errorCode = errorCode.getErrorCode(); this.errorDesc = errorCode.getErrorDesc(); } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param cause 嵌套的错误堆栈 */ public UserException(UserErrorCode errorCode, Throwable cause) { super(cause); this.errorCode = errorCode.getErrorCode(); this.errorDesc = errorCode.getErrorDesc(); } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param message 最好是自定义的详细描述信息,用于打日志,也可以和{ @link ErrorCode#getErrorDesc()}相同 * @param cause 嵌套的错误堆栈 */ public UserException(UserErrorCode errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode.getErrorCode(); this.errorDesc = errorCode.getErrorDesc(); }}
ps: 当你看完上述异常类的定义之后,那你一定知道为什么错误码定义要用枚举了,其实只要是除了字符串就行,否则会和官方默认的冲突,官方的默认字符串表示该异常的描述性信息,而我们的不仅要利用描述信息同时能在堆栈中打印的特性,还需要将我们错误码和错误码描述信息一同传入(这里的描述信息一般是传到到调用方的,不是用来打日志的,所以这两处的描述一般是不相同的),一次传入多个信息,所以自定义对象似乎是我们最好的选择。
如有不足之处欢迎留言交流。
参考文献
https://blog.csdn.net/qq_31463999/article/details/79220394