前言
- 本文说的自定义异常指的是继承了RuntimeException,专门用于Spring全局异常捕获处理的自定义异常类型,需要应用程序显示捕获并处理的异常不在本次讨论范围内。
- 本随笔经验总结,如有不足还原留言指正。
场景
- 使用java提供服务的后端系统,使用者(可能是web前端或者是第三方调用者)通过api形式进行调用。
目标
- 大多时候实现业务流程时总是可以假设所有操作都是成功的,因为如果有异常,直接根据异常类型转换为对应的自定义异常抛出,由全局异常处理从而结束当前业务逻辑即可。
- 根据系统异常类型制定对应的异常,以便进行全局异常处理。
- 异常一般伴随着对应的错误码,所以需要实现不同类型的异常对应不通类型的错误码,不得混用。
- 在进行全局异常捕获处理时,由于异常和错误码都实现了ErrorInfo接口,所以可以直接从该异常或者错误码中获取到对应的错误码和错误描述。
对异常进行分类
系统异常可以大致分为客户端异常和服务端异常,即任何异常都可以转换为这两类异常。
- 客户端异常:如参数错误等,建议http返回状态码为4xx。
- 服务端异常:如某些在意料之外并且不属于客户端异常操作引起的异常,建议http返回状态码为5xx。
异常体系设计类图
|
类/接口
|
说明
|
|---|---|
| AbstractException | AbstractException 表示自定义已知异常,即该异常可以完全不用显示处理,而是可以直接交给全局异常捕获自动处理,其中它有两个子类,一个是ClientException和SystemException,这两个子类分别与上面提到的可能的系统异常想对应。 |
| ClientException | ClientException表示"客户端异常",如接口方法不对或者参数不符合要求等。引发该异常时,除了返回对应的错误码与错误描述外,http响应状态码为4xx(一般为400)。 |
| SystemException | SystemException表示“服务端异常”,如图片下载失败导致我们系统本身出现不可控异常以及系统调用通道发生异常等。引发该异常时,除了返回对应的错误码与错误描述外,http响应状态码为5xx(一般为500)。 |
| ClientErrorCodeMark | 创建ClientException异常时所需的错误码抽象,即ClientException异常仅仅接受ClientErrorCodeMark类型的错误码。目的是防止不同类型的错误码混用,即客户端异常错误码不能用于服务端异常。 |
| SystemErrorCodeMark | 创建SystemException异常时所需的错误码抽象,即SystemException异常仅仅接受SystemErrorCodeMark类型的错误码。目的是防止不同类型的错误码混用,即客户端异常错误码不能用于服务端异常。 |
| ClientErrorCodeMark.Code SystemErrorCodeMark.Code |
Code分别是ClientErrorCodeMark和SystemErrorCodeMark的实现类,同时也是枚举对象。即在这里定义了各个错误码以及错误码对应的描述信息。 |
由以上类图可以看出,所有接口或者类都实现了ErrorInfo这个接口,而通过该接口可以拿到异常错误码和错误描述,也就是说,还是像以前一样,一旦引发异常,可以随时中断处理并且能返回给调用方一个合理的错误信息。
具体代码
/** * 公共接口,能获取错误描述和错误码. * <p> * 错误码和异常都要实现该接口,以便在全局异常处理时能方便地知道该异常对应的错误码和错误描述. * * @author DaiWang */ public interface ErrorInfo { /** * 获取异常对应的错误码. * * @return 错误码系统定义的错误码 */ String getErrorCode(); /** * 获取错误码对应的描述信息. * <p> * 如果本地对该描述信息进行设置,则本地的优先,否则自动从错误码系统获取错误码对应的描述信息. * </p> * @return 错误码对应的描述信息 */ String getErrorDesc(); }
/** * 自定义全局异常抽象.
* 作用是在全局异常处理时仅仅捕获该类型的异常即可,而不用分别处理ClientException(客户端异常)和SystemException(服务端异常)。
* * @author DaiWang */ public abstract class AbstractException extends RuntimeException implements ErrorInfo { public AbstractException() { super(); } public AbstractException(String message) { super(message); } public AbstractException(String message, Throwable cause) { super(message, cause); } public AbstractException(Throwable cause) { super(cause); } protected AbstractException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
/** * 外部传参数非法等客户端异常. * <p>即该异常由调用方导致.如调用通道时用户传递的图片过大等. * <p>直接提示用户相关错误即可 * <p> * Created by DaiWang on 2019/7/23. * </p> * * @author DaiWang */ public class ClientException extends AbstractException { private ErrorInfo errorCode; /** * 本地错误描述信息,其优先级高于{@link ErrorInfo}中的errorDesc,即如果该值不为空,则以该值为准 */ @Setter private String errorDesc; /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. */ public ClientException(ClientErrorCodeMark errorCode) { super(); this.errorCode = errorCode; } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param message 最好是自定义的详细描述信息,用于打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public ClientException(ClientErrorCodeMark errorCode, String message) { super(message); this.errorCode = errorCode; } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. */ public ClientException(ClientErrorCodeMark errorCode, Throwable cause) { super(cause); this.errorCode = errorCode; } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param message 最好是自定义的详细描述信息,用于打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public ClientException(ClientErrorCodeMark errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode; } @Override public String getErrorCode() { return errorCode.getErrorCode(); } /** * 获取错误码对应的描述信息. * <p> * 如果本地对该描述信息进行设置,则本地的优先,否则自动从错误码系统获取错误码对应的描述信息. * </p> * @return 错误码对应的描述信息 */ @Override public String getErrorDesc() { if (!StringUtils.isEmpty(errorDesc)) { return errorDesc; } return errorCode.getErrorDesc(); } @Override public String getMessage() { String message = super.getMessage(); if (StringUtils.isEmpty(message)) { message = this.getErrorDesc(); } return message; } }
/** * 客户端异常标记接口。 * 用于标记{@link ClientException}对象的错误码参数类型,防止错误码串用. * @author DaiWang */ public interface ClientErrorCodeMark extends ErrorInfo { /** * 用户操作异常错误码枚举 */ @Getter enum Code implements ClientErrorCodeMark { // region 参数错误 /** * 参数错误(可以笼统的将所有参数错误都包含在内,并重新定义错误描述,一般用于框架参数自定义错误验证) */ PARAMETER_ERROR("错误码1", "参数错误"), // region 操作过于频繁 错误码前缀: BOX_GA12xx FREQUENTLY("错误码2", "操作过于频繁,请稍候再试"), // endregion // ---------------------------------------------- /** * 照片质量校验不合格,请重新拍照后重试 */ BOX_GA0051("BOX_GA0051"), ; Code(String errorCode) { this.errorCode = errorCode; this.errorDesc = AssemblyUtil.getErrorOutDesc(errorCode); } Code(String errorCode, String errorDesc) { this.errorCode = errorCode; this.errorDesc = errorDesc; } /** * 错误码. */ private String errorCode; /** * 错误码对应的外部描述信息,该信息是通过错误码自动获取,并且直接返回给调用方. */ private String errorDesc; } /** * 快速构建一个自定义错误描述错误信息对象. * <p> * 一般用于相同的错误码,但是不同的错误描述信息(类似). * 比如常见的参数错误,但是可以使用参数错误的通用错误码在描述中提示调用者更具体的错误描述. * </p> * @param desc 新的错误描述 * @return 使用自定义描述替换原有描述的信息对象副本 */ default ClientErrorCodeMark as(String desc) { ErrorInfo errorInfo = this; return new ClientErrorCodeMark() { @Override public String getErrorCode() { return errorInfo.getErrorCode(); } @Override public String getErrorDesc() { return desc; } }; } }
/** * 转换后的系统异常. * <p> * 系统环境异常、数据库连接超时. * <p> * 提示用户系统异常,并且需要人工排查. * <p> * Created by DaiWang on 2019/7/23. * </p> * * @author DaiWang */ public class SystemException extends AbstractException { private ErrorInfo errorCode; /** * 本地错误描述信息,其优先级高于{@link ErrorInfo}中的errorDesc,即如果该值不为空,则以该值为准 */ @Setter private String errorDesc; /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. */ public SystemException(SystemErrorCodeMark errorCode) { super(); this.errorCode = errorCode; } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param message 最好是自定义的详细描述信息,用于打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public SystemException(SystemErrorCodeMark errorCode, String message) { super(message); this.errorCode = errorCode; } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. */ public SystemException(SystemErrorCodeMark errorCode, Throwable cause) { super(cause); this.errorCode = errorCode; } /** * @param errorCode 枚举的错误码类型,其中包含了错误码和对应的错误码描述信息. * @param message 最好是自定义的详细描述信息,用于打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public SystemException(SystemErrorCodeMark errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode; } @Override public String getErrorCode() { return errorCode.getErrorCode(); } /** * 获取错误码对应的描述信息. * <p> * 如果本地对该描述信息进行设置,则本地的优先,否则自动从错误码系统获取错误码对应的描述信息. * </p> * @return 错误码对应的描述信息 */ @Override public String getErrorDesc() { if (!StringUtils.isEmpty(errorDesc)) { return errorDesc; } return errorCode.getErrorDesc(); } @Override public String getMessage() { String message = super.getMessage(); if (StringUtils.isEmpty(message)) { message = this.getErrorDesc(); } return message; } }
/** * 标记接口. * 用于标记{@link SystemException}对象的错误码参数类型,防止错误码串用. * @author DaiWang */ public interface SystemErrorCodeMark extends ErrorInfo { /** * 我方系统异常错误码枚举 */ @Getter @AllArgsConstructor enum Code implements SystemErrorCodeMark { /** * 系统错误(本系统). */ SYSTEM_ERROR_LOCAL("错误码3", "系统异常"), /** * 系统内部错误(其他服务引起的错误,如通道异常) */ SYSTEM_ERROR_OTHER("错误码4", "系统繁忙"), // ----------------------------------------------------------------------------------------------------------------- /** * 系统内部错误 */ SYS_0001("错误码5"), /** * API不存在 */ SYSTEM_NOT_API("错误码6" , "API不存在"), /** * API不存在 */ HTTP_NOT_SUPPORTED("错误码7" , "HTTP请求方法不支持"), ; Code(String errorCode) { this.errorCode = errorCode; this.errorDesc = AssemblyUtil.getErrorOutDesc(errorCode); } /** * 错误码. */ private String errorCode; /** * 错误码对应的外部描述信息,该信息是通过错误码自动获取,并且直接返回给调用方. */ private String errorDesc; } /** * 快速构建一个自定义错误描述错误信息对象. * <p> * 一般用于相同的错误码,但是不同的错误描述信息(类似). * 比如常见的参数错误,但是可以使用参数错误的通用错误码在描述中提示调用者更具体的错误描述. * </p> * @param desc 新的错误描述 * @return 使用自定义描述替换原有描述的信息对象副本 */ default SystemErrorCodeMark as(String desc) { ErrorInfo errorInfo = this; return new SystemErrorCodeMark() { @Override public String getErrorCode() { return errorInfo.getErrorCode(); } @Override public String getErrorDesc() { return desc; } }; } }
通用且统一的响应体
该响应体为公司规范的实现,可能需要根据公司规范做相应的调整.
/** * 统一封装接口返回数据格式. * 使用时,直接通过 new 关键字自己方便地创建,非常直观,而不用再像以前一样还需要通过一个工具类生成. * * e.g * <pre><code> * // 根据错误码直接创建失败响应体. * new Result<Void>(ClientErrorCodeMark.Code.PARAMETER_ERROR); * * // 根据异常创建失败响应体 * AbstractException exception = new ClientException(ClientErrorCodeMark.Code.PARAMETER_ERROR) * // 根据捕获的异常创建 * new Result<Void>(exception); * </code></pre> * * @author DaiWang */ @Getter @ToString public class Result<T> { private static final int SUCCESS = 0; private static final int FAIL = 1; /** * 状态码. * 注意: 本字段是公司规范,实际上是可以省略的,因为根据{@linkplain #errorCode}字段的有误即可实现本字段的目的. */ private final int code; /** * 返回的具体数据. * <p> * 只有成功时才可能会返回. * 某些成功的操作可能不会返回该结果. * </p> */ private T data; /** * 错误码 */ private String errorCode; /** * 错误描述信息. */ private String errorDesc; /** * 请求ID,只要api响应就一定会返回. * 注意: 这里的请求Id是取子日志框架生成的id,没有初始化时应该为空. */ private final String requestId = MDC.get(Constants.TRANSACTION_ID); /** * 成功.并且没有业务返回数据. */ public Result() { this.code = SUCCESS; } /** * 成功.并且包含业务返回数据. */ public Result(T data) { this(); this.data = data; } /** * 异常. * 由于"错误码"和"异常"都实现了{@linkplain ErrorInfo},所以可以很方便地根据一个错误码(全局异常捕获到非自定义异常时可以直接使用错误码对象)或者一个捕获到的异常对象来创建一个失败响应体. * 注意: 理想情况下,异常时应该是没用业务数据的,所以异常时{@linkplain #data}字段必定为空. 即 Result<Void> * * @param errorInfo 上面定义的错误码或者异常对象 */ public Result(ErrorInfo errorInfo) { this.code = FAIL; this.errorCode = errorInfo.getErrorCode(); this.errorDesc = errorInfo.getErrorDesc(); } }