作者:京东零售 秦浩然

一、什么是反常

Java 言语依照过错严重性,从 throwale 根类衍生出 Error 和 Exception 两大派系。

Error(过错):

程序在履行进程中所遇到的硬件或操作体系的过错。过错对程序而言是致命的,将导致程序无法运转。常见的过错有内存溢出,jvm 虚拟机自身的非正常运转,calss 文件没有主办法。程序本生是不能处理过错的,只能依靠外界干涉。Error 是体系内部的过错,由 jvm 抛出,交给体系来处理。

Exception(反常):

程序正常运转中,能够意料的意外状况。比方数据库衔接中断,空指针,数组下标越界。反常呈现能够导致程序非正常停止,也能够预先检测,被捕获处理掉,使程序持续运转。Exception(反常)依照性质,又分为编译反常(受检反常)和运转时反常(非受检反常)。

◦编译反常:

又叫可查看反常,通常时由语法错和环境要素(外部资源)造成的反常。比方输入输出反常 IOException,数据库操作 SQLException。其特点是,Java 言语强制要求捕获和处理所有非运转时反常。通过行为规范,强化程序的健壮性和安全性。

◦运转时反常:

又叫不查看反常 RuntimeException,这些反常一般是由程序逻辑过错引起的,即语义错。比方算术反常,空指针反常 NullPointerException,下标越界 IndexOutOfBoundsException。运转时反常应该在程序测验期间被露出出来,由程序员去调试,而避免捕获。

二、处理反常方法

代码中,咱们最常见到的处理反常的方法就是:try-catch

        try {
            // 事务逻辑
        } catch (Exception e) {
            // 捕获到反常的逻辑
        }

或者是再进一步区别下反常类型:

        try {
            // 事务逻辑
        } catch (IOException ie) {
            // 捕获到IO反常的逻辑
        } catch (Exception e) {
            // 捕获到其他反常的逻辑
        }

三、怎么抛出反常

咱们通常能够用抛出反常的方法来操控代码流程,然后在网关处一致catch反常来回来过错code。这在必定程度上能够简化代码流程操控,如下所示:

    @Override
    public UserVO queryUser(Long id) {
        UserDO userDO = userMapper.queryUserById(id);
        if (Objects.isNull(userDO)) {
            throw new RuntimeException("用户不存在");    //用户不存在抛出反常
        }
        return userDO.toVo();
    }  

上面这种抛出反常的方法,尽管简化了代码流程,可是在存在多种过错场景时,没有办法细分详细的过错类型。如:用户不存在的过错、用户没有权限的过错;

聪明如你,必定想到了自界说反常,如下:

    @Override
    public UserVO queryUser(Long id) {
        UserDO userDO = userMapper.queryUserById(id);
        if (Objects.isNull(userDO)) {
            throw new UserNotFoundException();    //用户不存在抛出对应反常
        }
        if(!checkLicence(userDO)) {
            throw new BadLicenceException();    //用户无权限抛出对应反常
        }
        return userDO.toVo();
    }

的确,自界说反常能够处理过错场景细分的问题。进一步的,咱们能够对体系流程不同阶段、不同事务类型别离自界说反常,但这需求自界说很多的反常;

四、怎么优雅的抛出反常

上面的方法,能够区别出过错场景了,可是还存在一些缺陷。如:可读性差、需求界说很多的自界说反常;

那咱们下面就去优化上面的问题;

用断语增加代码的可读性;

    @Override
    public UserVO queryUser(Long id) {
        UserDO userDO = userMapper.queryUserById(id);
        Assert.notNull(userDO, "用户不存在");    //用断语进行参数的非空校验
        return userDO.toVo();
    }

断语尽管代码简练、可读性好,可是缺乏像上述自界说反常一样能够清晰区别过错场景,这就引出咱们的究极方案:自界说断语;

自界说断语;

咱们用自界说断语的方法,综合上面自界说反常和断语的优点,在断语失利后,抛出咱们制定好的反常。代码如下:

•自界说反常基本类

@Getter
@Setter
public class BaseException extends RuntimeException {
    // 呼应码
    private IResponseEnum responseEnum;
    // 参数信息
    private Object[] objs;
    public BaseException(String message, IResponseEnum responseEnum, Object[] objs) {
        super(message);
        this.responseEnum = responseEnum;
        this.objs = objs;
    }
    public BaseException(String message, Throwable cause, IResponseEnum responseEnum, Object[] objs) {
        super(message, cause);
        this.responseEnum = responseEnum;
        this.objs = objs;
    }
}

•自界说断语接口

public interface MyAssert {
    /**
     * 创建自界说反常
     *
     * @param objs 参数信息
     * @return 自界说反常
     */
    BaseException newException(Object... objs);
    /**
     * 创建自界说反常
     *
     * @param msg  描绘信息
     * @param objs 参数信息
     * @return 自界说反常
     */
    BaseException newException(String msg, Object... objs);
    /**
     * 创建自界说反常
     *
     * @param t    接收验证反常
     * @param msg  描绘信息
     * @param objs 参数信息
     * @return 自界说反常
     */
    BaseException newException(Throwable t, String msg, Object... objs);
    /**
     * 校验非空
     *
     * @param obj 被验证目标
     */
    default void assertNotNull(Object obj, Object... objs) {
        if (obj == null) {
            throw newException(objs);
        }
    }
    /**
     * 校验非空
     *
     * @param obj 被验证目标
     */
    default void assertNotNull(Object obj, String msg, Object... objs) {
        if (obj == null) {
            throw newException(msg, objs);
        }
    }
}

上述代码咱们能够看出基本设计,就是在咱们自界说断语失利后抛出咱们自界说反常。

下面是详细的实现案例:

•自界说事务反常类,承继自反常基本类

public class BusinessException extends BaseException {
    public BusinessException(IResponseEnum responseEnum, Object[] args, String msg) {
        super(msg, responseEnum, args);
    }
    public BusinessException(IResponseEnum responseEnum, Object[] args, String msg, Throwable t) {
        super(msg, t, responseEnum, args);
    }
}

•呼应code枚举接口界说

public interface IResponseEnum {
    /**
     * 回来code码
     *
     * @return code码
     */
    String getCode();
    /**
     * 回来描绘信息
     *
     * @return 描绘信息
     */
    String getMsg();
}

•自界说事务反常类断语界说,实现自界说断语失利后对应的自界说反常的界说;

public interface BusinessExceptionAssert extends IResponseEnum, MyAssert {
    @Override
    default BaseException newException(Object... args) {
        return new BusinessException(this, args, this.getMsg());    //断语失利后,抛出自界说反常
    }
    @Override
    default BaseException newException(String msg, Object... args) {
        return new BusinessException(this, args, msg);              //断语失利后,抛出自界说反常
    }
    @Override
    default BaseException newException(Throwable t, String msg, Object... args) {
        return new BusinessException(this, args, msg, t);           //断语失利后,抛出自界说反常
    }
}

•用枚举的方法,替代BadLicenceException、UserNotFoundException自界说反常。

public enum ResponseEnum implements IResponseEnum, BusinessExceptionAssert {
    BAD_LICENCE("0001", "无权拜访"),
    USER_NOT_FOUND("1001", "用户不存在"),
    ;
    private final String code, msg;
    ResponseEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    @Override
    public String getCode() {
        return code;
    }
    @Override
    public String getMsg() {
        return msg;
    }
}

运用实例

自界说断语失利抛出自界说反常

    @Override
    public UserVO queryUser(Long id) {
        UserDO userDO = userMapper.queryUserById(id);
        ResponseEnum.USER_NOT_FOUND.assertNotNull(userDO);    //自界说断语失利抛出自界说反常
        return userDO.toVo();
    }

网关处一致catch反常,识别反常场景

    public static void main(String[] args) {
        UserService userService = new UserServiceImpl(new UserMapperImpl());
        UserController userController = new UserController(userService);
        try {
            UserVO vo = userController.queryUser(2L);               //履行事务逻辑
        } catch (BusinessException e) {
            System.out.println(e.getResponseEnum().getCode());      //呈现反常,过错code:1001
            System.out.println(e.getMessage());                     //呈现反常,过错msg:用户不存在
        }
    }

五、怎么优雅的处理反常

网关处一致处理反常,这归于惯例操作,这里不再赘述,简略举例如下:

@ControllerAdvice
public class BusinessExceptionHandler {
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public Response handBusinessException(BaseException e) {
        return new Response(e.getResponseEnum().getCode(), e.getResponseEnum().getMsg());    //一致处理反常
    }
}

综上,咱们采用自界说断语的方法,结合了断语的可读性高的优势和自界说反常区别过错场景的优势。并且,有新增的过错场景,咱们只需求在过错码枚举中新增对应枚举即可。