寿险业务系统异常处理方案

我们的系统使用的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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【江湖】三津

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值