作者:京东零售 秦浩然
一、什么是反常
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()); //一致处理反常
}
}
综上,咱们采用自界说断语的方法,结合了断语的可读性高的优势和自界说反常区别过错场景的优势。并且,有新增的过错场景,咱们只需求在过错码枚举中新增对应枚举即可。