我们的系统使用的java语言开发,基于Spring框架搭建的业务中台,在讨论业务系统异常处理策略之前,先把java的异常机制进行简单说明。
一、Java的异常机制
1.Java异常分类
【Error】是系统错误或者编译错误,常见的虚拟机运行错误、栈溢出错误、内存溢出错误都是属于error,这种程序无法处理,发生后会导致jvm终止线程
【Exception】是程序中产生的错误,程序本身可以捕获并且处理。通常会分为运行时异常(非受检异常)和非运行时异常(受检异常),受检异常程序必须要处理(try-catch 或者继续抛出),非受检异常程序可以不处理,会自动向上抛出,直至main方法或者Thread.run方法,终止该线程。
2. Java的异常处理方式
(1)调用方通过try - catch - finally处理,示例代码如下:
try
{
可能会发生的异常
}catch(异常类型 异常名(变量)){
针对异常进行处理的代码
}catch(异常类型 异常名(变量)){
针对异常进行处理的代码
}...
[finally{
释放资源代码;
}]
(2)throws 调用方补处理,直接将异常在方法声明中抛出,交由上层处理,示例代码如下:
public void testExceptionThrow throws NullPointerException{
throw new NullPointerException();
}
二、Spring的异常机制
Spring有一套自带的视图错误处理机制,借助SpringMVC的视图控制能力,通过一些异常的处理Resolver来进行错误页面的跳转。而在中台系统建设中,则需要用到Spring提供的自定义异常的处理能力,后端使用的异常处理机制主要以下两种
1. @ControllerAdvice+@ExceptionHandler处理全局异常
实现方式是自定义一个异常处理类,只需要在该类上标记@ControllerAdvice即可。
同时要在执行异常处理的方法上标记@ExceptionHandler。这样在发生了指定异常时可以找到响应的异常处理方法进行处理。此种模式的底层是 ExceptionHandlerExceptionResolver 支持的,代码示例如下:
/**
* 处理整个web controller的异常
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class}) // 可以处理多种异常
public String handleArithmeticException() {
log.info("处理异常");
// 也可以返回ModelAndView类型对象,因为在处理异常相关源码中最后也会渲染视图转化为ModelAndView
return "error";
}
@ExceptionHandler({NullPointerException.class})
public String handleNullPointerException(Exception e) {
log.info("处理异常");
return "error";
}
}
@ControllerAdvice 注解的原理是SpringAOP提供的,是将Controller层的方法作为切面,从而对Controller层方法进行拦截处理,如果是前后端分离的项目也可以使用 @RestControllerAdvice 注解。
2. 自定义实现 HandlerExceptionResolver 处理异常
可以作为默认的全局异常处理规则(注意:设置为最高优先级会顶替掉SpringBoot原生的异常处理规则)
// SpringBoot底层会优先调用SpringBoot定义的异常处理器(ExceptionResolver)
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
response.sendError(511, "xxx错误");
} catch (IOException e) {
e.printStackTrace();
}
ModelAndView modelAndView = new ModelAndView();
return modelAndView;
}
}
三、业务系统中的异常处理
异常是可以继承的,业务系统中都会通过继承异常实现自己的自定义异常,而为了简化开发,减少程序员的工作量,自定义异常都会实现RuntimeException ,这样程序中就不需要显性处理,如有需要自己捕获处理该异常,但这种潜规则会导致很多程序员不知道异常该如何使用如何设计。下面我就分享下寿险中台中的异常处理机制,可以给大家提供一种异常处理的参考方案。
1.设计异常机制前的知识准备
【为什么要自定义异常】
Java是面向对象的语言,区别于面向过程的语言,很多处理结果和信息不仅可以像面向过程的语言的返回值传递,还可以通过参数对象、上下文变量传递,而java提供的异常机制就是在不影响方法定义的出参、入参的情况下定义错误信息的传递机制。
【系统中的错误码和异常的关系】
异常是一种快捷方便的错误传递方式,而外部服务接口交互不能通过异常的方式传递。
(1)错误码 : 接口交互的错误传递方式,一般都是由一个Result的DTO 通过errorCode和errorMessage来承载。
(2)异常 : 应用内部的错误传递方式,可以由框架统一处理,调用方也可以根据自己的需要去差异化catch进行业务处理。
【为什么会选择集成RuntimeException】
RuntimeException不需要程序处理,可以交由框架自动处理,而且Spring的异常机制就是针对RuntimeException进行的,更加方便了程序员的开发工作。由于RuntimeException的无感也会带来一些问题,如果不在方法中声明或者注释中说明异常情况,调用方很容易忽略了异常情况的逻辑和应对,致使系统的健壮性降低。
【系统异常还有没有必要自定义,有没有必要继承RuntimeException】
先说结论系统异常自定义还是有必要的,因为很多系统异常都是受检异常,尤其使用一些组件或者功能时,数组越界、ClassNotFound等,针对这类异常程序必须要catch或者throw 处理,会给开发带来很大的处理工作。而系统异常的处理往往逻辑比较统一,完全可以交由框架在拦截层统一处理,所以在业务系统中将系统异常统一定义转换,可以极大降低对开发人员的要求,同时也可以满足资深程序员对各类异常单独处理的需求。
【异常中需要注意的事项】
(1)主动throw异常,异常中需要带有上下文信息;
(2)程序中尽量不要产生error,error是无法处理的,会导致线程终止,exp. 方法参数中使用原生类型int,而请求方传的参数是Integer,当参数为null的时候就会触发error,导致线程终止;
2.寿险中台的异常规范
【寿险中台异常的分类】
常见的业务系统中都会定义自己的业务异常、系统异常,寿险中台也是使用了众安的common包中的异常定义的BizException和ServiceException作为寿险中台异常的父类,这样做的好处时即使寿险中台自定义了异常,但整体异常框架仍然是在众安技术部的规范内,实现自己的自定义需求的同时,不会影响技术框架的能力。
业务系统通常是自定义业务异常,但针对特殊的系统异常有特殊处理的话也可以扩展ServiceException(极少数,比如需要识别一些中间件的特殊异常进行中间件调用方式的替换等)。所以业务系统自定义的异常归类如下:
【系统异常规范】
每个系统必须有一个统一的Error枚举类,以理赔为例会在common包中定义ClaimErrorEnum;自定义异常构造方法参数中必须要有此枚举类。
在说明业务系统错误枚举类之前,在提下业务错误枚举的父类接口BaseResultCode。由于枚举无法继承,所以一些通用的内容通过接口的方式让业务枚举实现,赋能给枚举类。业务错误枚举类通过接口实现继承了错误码的组装方法,业务系统只需要关注自己的3位错误码即可,同时也规范了业务枚举类的方法行为,可以统一拦截标准化处理。
接口BaseResultCode代码如下:
/**
* 错误码枚举的实现基础接口 <br/>
* 各业务系统的自定义枚举类需要实现此接口 <br/>
*
* @author guosenlin
* @date 2022/2/21 11:12
*/
public interface BaseResultCode {
/**
* 部门编码:技术服务中心编码
*/
String DEPARTMENT_CODE = "10";
/**
* 应用编码:未知的应用编码
*/
String UNKNOWN_APP_CODE = "00";
/**
* 返回错误编码,定义为三位,自定义 2 是业务错误码 9 是系统错误码
*
*
* @return
*/
String getCode();
/**
* 返回错误描述
*
* @return
*/
String getMsg();
/**
* 应用编码
*
* @return
*/
String getAppCode();
/**
* 返回错误编码,由部门编码+应用编码+自定义编码拼接完成 <br/>
* 1、部门编码:取默认值DEPARTMENT_CODE <br/>
* 2、应用编码:子类实现的getAppCode()方法返回,一个应用是固定值 <br/>
* 3、自定义编码:子类枚举实现getCode() <br/>
*
* @return
*/
default String errorCode() {
String appCode = getAppCode();
String code = getCode();
StringBuilder sb = new StringBuilder();
sb.append(DEPARTMENT_CODE);
// 00代表未知
sb.append(appCode != null ? appCode : UNKNOWN_APP_CODE);
sb.append(code);
return sb.toString();
}
/**
* 获得错误描述
*
* @return
*/
default String errorDesc() {
return getMsg();
}
}
理赔服务业务错误枚举ClaimErrorEnum代码如下:
/**
* 理赔错误码枚举类 <br/>
* code:错误代码 <br/>
* msg:错误描述 <br/>
*
* @author nidazhang
* @date 2022-11-14
*/
@Getter
@AllArgsConstructor
public enum ClaimErrorCodeEnum implements BaseResultCode {
/** 201:理赔报案失败 */
REPORT_FAILED_ERROR("201", "理赔报案失败"),
/** 202:未查询到该报案号对应的案件信息 */
NOT_QUERY_REPORT_INFO_ERROR("202", "未查询到该报案号对应的案件信息"),
/** 203:案件状态非法 */
ILLEGAL_REPORT_STATUS_ERROR("203", "案件状态非法"),
/** 204:案件存在分支业务 */
REPORT_HAS_BRANCH_BUSINESS_ERROR("204", "案件存在分支业务"),
/** 205:调用保单锁单并抄单返回为空 */
COPYING_POLICY_RETURN_NULL_ERROR("205", "调用保单锁单并抄单返回为空"),
/** 206:批量解锁失败 */
BATCH_UNLOCK_FAILED_ERROR("206", "批量解锁失败"),
/** 207:案件已注销或者已结案,不允许对案件进行操作 */
REPORT_STATUS_NOT_ALLOW_OPERATE_ERROR("207", "案件已注销或者已结案,不允许对案件进行操作"),
/** 208:未查找到该分案或者材料已齐全,禁止补传 */
PROHIBIT_MATERIAL_UPLOAD_ERROR("208", "未查找到该分案或者材料已齐全,禁止补传"),
/** 901:调用保单锁单并抄单异常 */
CALL_LOCK_AND_QUERY_POLICY_ERROR("901", "调用保单锁单并抄单异常"),
/** 902:调用批量解锁系统异常 */
CALL_BATCH_UNLOCK_ERROR("902", "调用批量解锁系统异常"),;
private String code;
private String msg;
/**
* 获得理赔系统的项目编码,为固定常量值
*
* @return
*/
@Override
public String getAppCode() {
return ClaimReportConstants.CLAIM_APP_CODE;
}
}
自定义异常代码如下:
异常基础类:
/**
* 异常基础类
*
* @author guosenlin
* @data 2021/7/30 14:36
*/
public abstract class BasicException extends RuntimeException {
/**
* 异常错误码枚举
*/
protected BaseResultCode errorCodeEnum;
/**
* 异常错误信息描述
*/
protected String errorMsg;
/**
* 异常上下文信息
*/
protected Map<String,Object> exceptionContext;
/**
* 获得错误码枚举信息
*
* @return
*/
public BaseResultCode getErrorCodeEnum() {
return errorCodeEnum;
}
/**
* 获得错误信息
*
* @return
*/
public String getErrorMsg() {
if (errorMsg != null) {
return errorMsg;
}
if (errorCodeEnum != null) {
return errorCodeEnum.errorDesc();
}
return "";
}
/**
* 获得错误码信息
*
* @return
*/
public String getErrorCode() {
if (errorCodeEnum != null) {
return errorCodeEnum.errorCode();
}
return "";
}
/**
* 获得异常上下文信息
*
* @return
*/
public Map<String,Object> getExceptionContext() {
return exceptionContext;
}
public BasicException() {
super();
}
public BasicException(String message) {
super(message);
}
public BasicException(Throwable cause) {
super(cause);
}
public BasicException(String message, Throwable cause) {
super(message, cause);
}
@Override
public String getMessage() {
StringBuilder sb = new StringBuilder();
sb.append(getErrorCode());
sb.append(":");
sb.append(getErrorMsg());
sb.append(",");
String message = super.getMessage();
sb.append(message != null ? message : "");
return sb.toString();
}
}
理赔服务自定义异常示例:
/**
* 理赔抄单异常
*
* @author guosenlin
* @data 2021/7/30 14:37
*/
public class ClaimCopyPolicyException extends BasicException {
private static final long serialVersionUID = 2237743543787228870L;
public ClaimCopyPolicyException(String errMsg, Object... contextInfo) {
this(errMsg, null, contextInfo);
}
public ClaimCopyPolicyException(Throwable e, Object... contextInfo) {
this(null, e, contextInfo);
}
public ClaimCopyPolicyException(String errMsg, Throwable e, Object... contextInfo) {
super(e);
this.errorMsg = errMsg;
this.contextInfo = contextInfo;
}
}
【业务异常处理机制】
(1)系统框架异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseBean<?> methodArgumentNotValidErrorHandler(HttpServletRequest req,
MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
StringBuilder errorMsg = new StringBuilder();
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
allErrors.forEach(error -> errorMsg.append(error.getDefaultMessage()).append("!"));
}
log.info("参数校验失败,reqMethod:{},URI:{},错误信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(), errorMsg,
ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(ResultCodeEnum.PARAM_EXCEPTION, errorMsg.toString());
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseBean<?> illegalArgumentExceptionHandler(HttpServletRequest req, IllegalArgumentException e) {
String message = e.getMessage();
log.info("参数校验失败,reqMethod:{},URI:{},错误信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(), message,
ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(ResultCodeEnum.PARAM_EXCEPTION, message);
}
@ExceptionHandler(BindException.class)
public ResponseBean<?> bindErrorHandler(HttpServletRequest req, BindException e) {
BindingResult result = e.getBindingResult();
StringBuilder errorMsg = new StringBuilder();
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
allErrors.forEach(error -> errorMsg.append(error.getDefaultMessage()).append("!"));
}
log.info("参数校验失败,reqMethod:{},URI:{},错误信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(), errorMsg,
ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(ResultCodeEnum.PARAM_EXCEPTION, errorMsg.toString());
}
@ExceptionHandler(value = BizException.class)
public ResponseBean<?> bizErrorHandler(HttpServletRequest req, BizException e) {
log.info("业务异常,reqMethod:{},URI:{},上下文信息:{},异常信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(),
LogUtils.toJsonOrElseToString(e.getContextInfo()), e.getErrorMsg(), ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(e.getErrorCodeEnum(), e.getErrorMsg());
}
@ExceptionHandler(value = ServiceException.class)
public ResponseBean<?> serviceErrorHandler(HttpServletRequest req, ServiceException e) {
log.warn("服务异常,reqMethod:{},URI:{},上下文信息:{},异常信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(),
LogUtils.toJsonOrElseToString(e.getContextInfo()), e.getErrorMsg(), ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(e.getErrorCodeEnum(), e.getErrorMsg());
}
@ExceptionHandler(value = BasicException.class)
public ResponseBean<?> basicExceptionHandler(HttpServletRequest req, BasicException e) {
log.warn("服务异常,reqMethod:{},URI:{},上下文信息:{},异常信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(),
LogUtils.toJsonOrElseToString(e.getContextInfo()), e.getErrorMsg(), ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(e.getErrorCodeEnum(), e.getErrorMsg());
}
@ExceptionHandler(value = Throwable.class)
public ResponseBean<?> defaultErrorHandler(HttpServletRequest req, Throwable e) {
log.error("系统异常,reqMethod:{},URI:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(),
ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(ResultCodeEnum.UNKNOWN_EXCEPTION, e.getMessage());
}
}
(2)主动捕获特殊处理
//抄单并锁单
List<ClaimPolicyBO> claimPolicyList = multiplePolicyLockIntegration.queryPolicySnapAndLock(claimReportBO);
try {
//生成立案号
String registerNo = bizNoGenarateManager.generateRegistNo();
claimReportBO.setRegisterNo(registerNo);
claimReportBO.setRegisterTime(LocalDateTime.now());
claimReportBO.setStatus(CaseStatusEnum.FINISHED_REGIST);
//抄单填充报案号以及分案号以及锁单标记
for (ClaimCaseBO claimCaseBO : claimReportBO.getSubCaseList()) {
for (ClaimReportPolicyBO casePolicyRelaBO : claimCaseBO.getCasePolicyRelationList()) {
setPolicyLevelReportNo(claimReportBO, claimPolicyList, claimCaseBO, casePolicyRelaBO);
}
}
//入库保存
reportRepositoryService.saveClaimRegist(claimReportBO);
} catch (Exception exception) {
//入库或者生成号码等异常时候,保单解锁掉
multiplePolicyLockIntegration.batchUnlockPolicy(claimReportBO);
throw new ServiceException(ClaimErrorCodeEnum.CLAIM_REGISTER_SYS_002_ERROR, exception, claimReportBO.getReportNo());
}
(3)异常信息在接口层的转换处理
1)针对系统异常处理:通过异常切面,将异常转换为错误码
@ExceptionHandler(BindException.class)
public ResponseBean<?> bindErrorHandler(HttpServletRequest req, BindException e) {
BindingResult result = e.getBindingResult();
StringBuilder errorMsg = new StringBuilder();
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
allErrors.forEach(error -> errorMsg.append(error.getDefaultMessage()).append("!"));
}
log.info("参数校验失败,reqMethod:{},URI:{},错误信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(), errorMsg,
ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(ResultCodeEnum.PARAM_EXCEPTION, errorMsg.toString());
}
2)针对业务异常处理:获取异常的错误码,通过统一的包装类进行返回
@ExceptionHandler(value = BasicException.class)
public ResponseBean<?> basicExceptionHandler(HttpServletRequest req, BasicException e) {
log.warn("服务异常,reqMethod:{},URI:{},上下文信息:{},异常信息:{},异常堆栈:{}", req.getMethod(), req.getRequestURI(),
LogUtils.toJsonOrElseToString(e.getContextInfo()), e.getErrorMsg(), ExceptionUtils.getStackTrace(e));
return ResponseBean.fail(e.getErrorCodeEnum(), e.getErrorMsg());
}
【寿险中台异常处理方案】
【寿险中台自定义异常使用规范】
(1)主动封装抛出自定义异常, 必须带有环境上下文信息,上下文信息要包含发生异常点的具体信息;比如在遍历保单险种的时候发生数据不合法系统异常,上下文中应该包含遍历的保单险种号,而不只是保单号信息;
(2)主动抛出异常无需在打印相关异常日志,因为异常中已包含堆栈信息和上下文信息,最终会在捕获处或者系统拦截器处打印;此时打印属于重复打印,会无谓增加日志量;
(3)只允许主动抛出自定义业务异常,不允许主动抛出java原生异常;可以将其他系统异常转化为自定义系统异常,原则上不允许主动抛出自定义的系统异常,比如以下代码使用的是java原生的IllegalStateException来封装业务枚举不存在异常,会导致异常捕获处理的复杂度和困难增加。
反例错误代码
(4)捕获异常必须进行有效业务处理,否则不允许catch自定义异常。有效业务处理包括打印error日志触发告警,保存异常记录数据,调用其他业务方法或者将受检异常转为自定义系统异常等;不允许捕获异常后只是做runtime类的异常转换或者打印日志等无用行为;
(5)程序员主动抛出异常必须清晰区分系统异常、业务异常和正确的错误枚举,业务异常和系统异常会导致处理逻辑的差异,影响业务处理结果;
(6)定义方法时如果会抛出自定义异常,必须在方法声明中声明异常信息,并在注释中说明不同错误枚举的产生业务场景,以便调用方根据情况自行决定异常处理策略;方法中调用其他服务产生的自定义异常也应在方法中声明;
(7)寿险中台的默认事务处理是在Facade层,当没有明确的事务代码或注解,事务会在facade层统一提交,异常传递至平台默认拦截器层时会导致事务回滚;
(8)非必要不允许自定义异常,必要场景为需要针对某类业务错误进行特殊业务处理,这种情况下通过自定义异常方便系统通过catch异常的方式实现。禁止只是为了某一通用异常业务概念进行自定义封装,比如核保不通过属于行业内比较通用的业务规则错误,但如果没有针对核保不通过的异常进行捕获处理需求,只需要使用通用的BizException+核保不通过错误枚举承载即可。
(9)寿险中台通用错误码规范
寿险中台错误码由 三部分组成 系统编码 + 应用编码 + 具体错误码,具体错误码包含通用错误码和自定义错误码,通用错误码包含常见的业务参数不合法、权限不足、不符合业务、系统未知错误等通用错误码。以寿险中台新契约核保不通过为例,寿险中台系统编码为SX,新契约编码为NCS,核保不通过的错误码排位201 ,则此最终接口错误码为 SX-NCS-001。
(10)应用自定义错误码需要遵守通用的号段规则
2XX代表业务错误码,应用依次编排自定义业务错误码;
9XX代表系统错误码,应用依次编排自定义系统错误码。
参考文章:
https://blog.csdn.net/qq_51628741/article/details/125873733