我们开发过程中,不管是通用代码的开发还是业务代码的编写,都涉及到异常的处理,如果不对异常进行封装处理的话,会导致我们的代码十分的不雅观,比较low,所以一个好的全局异常处理的封装不仅仅能加快我们的开发效率,并且也能让我们的代码逼格更高点,何乐而不为呢?
首先我们来看下一般情况下代码的处理
/**
* 方法定义声明式异常,明确方法调用者需要手动处理异常
*
* @throws Exception
*/
public void demonstrateExceptionThrows() throws Exception {
System.out.println(1 / 0);
}
/************************************************************/
@Autowired
BizService bizService;
public String call() {
try {
this.bizService.demonstrateExceptionThrows();
} catch (Exception e) {
return "error:" + e.getMessage();
}
return "ok";
}
上面的代码我们使用try...catch...来捕获处理异常,如果大量的代码需要手动去处理异常,那么代码就先的很臃肿,代码冗余量也大增,这样的代码不仅阅读不赏心悦目,也显得我们的技术不咋滴啊。
SpringMVC提供的解决方案
那有没有一种解决方案,能很优雅的实现异常的捕捉,让我们更专注于业务层面的开发呢?有的,以前实现起来可能比较繁琐,万幸的是我们有了Spring这一利器,Spring本身为我们提供了一个解决方案来全局处理异常的方案,让我们更好地专注于业务层面的开发。
接下来我们来介绍一下今天的主角@ControllerAdvice
和@ExceptionHandler
;
网上关于@ControllerAdvice
和@ExceptionHandler
的介绍一大堆,我们主要不是为了介绍这两个注解的实现原理,我们只是简单接收这两个注解在我们封装的全局异常处理的方案中是如何运用的,能实现什么样的一个效果
具体的原理和使用方法可以参考如下的博文
Spring MVC之@ControllerAdvice详解
异常的判断和抛出
既然Spring为我们解决了全局异常的处理,那么我们这里的任务就是需要设计一行代码来捕获异常,并且向外抛出自定义的异常
说到一行代码就抛出异常,各位是不是第一个就想到Assert
呢;是的,Assert的内部实现很简单,就是判断,不符合条件就抛出异常,但是也有相当大的局限性,Assert
抛出的异常全部都是IllegalArgumentException
异常,这个跟我们的需求不太符合,所以我们自己设计出一个类似Assert
的类,但是可以手动返回自定义的异常以及异常编码和异常信息
我们的异常的判断和抛出需要满足以下几个需求:
- 同意定义自定义异常的创建
- 异常的编码能够很方便的扩展,并且异常的信息能够自定义
- 支持多个自定义异常的创建,并且对扩展开放,定义的异常自动进行全局了拦截
好了,有句话说的话 talk is cheap, show me code
,下面我们来贴出主要的代码
使用的SpringBoot和jdk1.8环境
- 首先定义出自定义的Assert来实现我们的一行代码抛出异常,该接口中定义了创建异常的犯法,并且所以异常对于非空的判断,后面我们自定义的异常都需要实现该接口,并且提供自定义异常实例化的方法。
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: BizAssert
* @Package: com.amos.bizexception.exception
* @author: zhuqb
* @Description: 自定义的断言
* <p/>
* 仿照Assert的思路来提供异常的处理,让代码更优美
* <p/>
* 接口定义创建异常的定义方法,并且默认提供异常的判断方法
* @date: 2019/7/11 0011 上午 8:58
* @Version: V1.0
*/
public interface BizAssert {
/**
* 定义创建异常的方法
*
* @param args 异常的信息
* @return
*/
BaseException newException(Object... args);
/**
* 定义创建好友异常信息的异常的方法
*
* @param throwable 异常的信息
* @param args 异常的msg
* @return
*/
BaseException newException(Throwable throwable, Object... args);
/**
* 判断对象是否是空
*
* @param object 带判断是否为空的对象
*/
default void notNullAssert(Object object) {
if (StringUtils.isEmpty(object)) {
throw this.newException(object);
}
}
/**
* 判断对象是否为空
*
* @param object 带判断是否为空的对象
* @param msg 异常返回的信息
*/
default void notNullAssert(Object object, String msg) {
if (StringUtils.isEmpty(object)) {
throw this.newException(msg);
}
}
/**
* 判断对象是否为空(集合没有元素)
*
* @param object 带判断是否为空的对象
* @param msg 异常返回的信息
*/
default void notEmptyAssert(Object object, String msg) {
if (StringUtils.isEmpty(object)) {
throw this.newException(msg);
}
// 如果是集合,则判断集合里面的元素是否存在
if (object instanceof Collection) {
int size = ((Collection) object).size();
if (size == 0) {
throw this.newException(msg);
}
}
// 如果是数组,则判断数组元素是否存在
if (object.getClass().isArray()) {
int length = ((Object[]) object).length;
if (length == 0) {
throw this.newException(msg);
}
}
}
}
- BaseException继承了RuntimeException类,这里是我们所有类的基类,也是全局拦截所有自定义异常类的基础,主要是通过多态的方式来实现拦截所有子类的目的,获取子类的自定义异常的编码和信息
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: BaseException
* @Package: com.amos.bizexception.exception.bean
* @author: zhuqb
* @Description: 自定义业务处理的基类异常
* @date: 2019/7/11 0011 上午 10:11
* @Version: V1.0
*/
public class BaseException extends RuntimeException {
private static final long serialVersionUID = 6433183307699823302L;
/**
* 异常的编码和信息
*/
private IResult result;
/**
* 消息定义的参数
*/
private Object[] args;
/**
* 基类异常的构造器
*
* @param result
*/
BaseException(IResult result) {
super(result.getMsg());
this.result = result;
}
/**
* 通过code和msg来实例化基类异常信息
*
* @param code
* @param msg
*/
BaseException(int code, String msg) {
super(msg);
this.result = new IResult() {
@Override
public int getCode() {
return code;
}
@Override
public String getMsg() {
return msg;
}
};
}
/**
* 基类异常实例化,附带异常参数
*
* @param result
* @param args
* @param msg
*/
BaseException(IResult result, Object[] args, String msg) {
super(msg);
this.result = result;
this.args = args;
}
/**
* 基类异常实例化,附带异常类信息
*
* @param result
* @param args
* @param msg
* @param throwable
*/
BaseException(IResult result, Object[] args, String msg, Throwable throwable) {
super(msg, throwable);
this.result = result;
this.args = args;
}
}
- 基类中我们定义了IResult这个接口,这个接口只定义了两个方法,主要是为了我们自定义异常的编码和信息服务的,编码代表各种不同种类的异常,信息则是异常的具体说明,然后在基类中设定不同的参数的实例化方法
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: IResult
* @Package: com.amos.bizexception.exception.type
* @author: zhuqb
* @Description: 该接口定义了异常信息返回的通用字段
* <p/>
* 让子类去实现该接口,返回异常的编码和信息
* 这样做的好处是用户可以自定义实现异常的编码和信息,系统的扩展性更强
* @date: 2019/7/11 0011 上午 10:13
* @Version: V1.0
*/
public interface IResult {
/**
* 异常的编码
*
* @return
*/
int getCode();
/**
* 异常的信息
*
* @return
*/
String getMsg();
}
- 基类的相关方法和属性我们定义好了,现在我们开始来实现我们的自定义异常,该异常继承
BaseException
异常,并且定义构造器方便实例化
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: BizException
* @Package: com.amos.bizexception.exception.bean
* @author: zhuqb
* @Description: 自定义业务异常
* <p/>
* 异常继承基类异常,并且实现对应的构造器来初始化自定义业务异常
* @date: 2019/7/11 0011 上午 10:41
* @Version: V1.0
*/
public class BizException extends BaseException {
private static final long serialVersionUID = -7118967757594184955L;
/**
* 自定义业务异常的构造器
*
* @param result
*/
public BizException(IResult result) {
super(result);
}
/**
* 自定义业务异常的构造器
*
* @param code
* @param msg
*/
public BizException(int code, String msg) {
super(code, msg);
}
/**
* 自定义业务异常的构造器
*
* @param result
* @param args
* @param msg
*/
public BizException(IResult result, Object[] args, String msg) {
super(result, args, msg);
}
/**
* 自定义业务异常的构造器
*
* @param result
* @param args
* @param msg
* @param throwable
*/
public BizException(IResult result, Object[] args, String msg, Throwable throwable) {
super(result, args, msg, throwable);
}
}
- 上面我们已经定义好了所有基类的
Assert
,其实所有基类的Assert
就已经能实现全部捕获异常的要求了,但是不方便扩展,我们的需求是支持各种自定义异常抛出各自的异常,并且能够自定义异常编码和消息, 这里我们先来让我们的程序可以支持创建各种不同的自定义异常
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: BizExceptionAssert
* @Package: com.amos.bizexception.exception
* @author: zhuqb
* @Description: 自定义业务异常断言
* <p/>
* 该接口异常异常信息和自定义业务断言接口, 继承这两个接口
* 1. 获取异常的编码和信息接口,方便实现类自定义异常的编码和信息
* 2. 获取定义异常的创建方法,并且重写创建异常实例的方法,返回对应的业务异常的实例
* 3. 注意:此处的异常的编码和信息我们可以暂时不处理,交给子类来自定义异常编码和信息
* @date: 2019/7/11 0011 上午 10:48
* @Version: V1.0
*/
public interface BizExceptionAssert extends IResult, BizAssert {
/**
* 实现创建异常的方法, 返回自定义异常实例对象
*
* @param args 异常的信息
* @return
*/
@Override
default BaseException newException(Object... args) {
String msg = MessageFormat.format(this.getMsg(), args);
return new BizException(this, args, msg);
}
/**
* 实现创建异常的方法, 返回自定义异常实例对象
*
* @param throwable 异常的信息
* @param args 异常的消息
* @return
*/
@Override
default BaseException newException(Throwable throwable, Object... args) {
String msg = MessageFormat.format(this.getMsg(), args);
return new BizException(this, args, msg, throwable);
}
}
- 上面的注释说的没有实现异常编码和信息的定义,这里我们优化下设计,同一种自定义异常也支持异常编码和信息的自定义,说白了,我们这里就是将自定义异常和异常编码和信息解耦了,这样方便我们通过定义不同的对象来组合自定义异常和异常编码和信息,这个其实体现了设计模式中开闭原则的开原则,那么闭原则我们怎么处理呢?别急,还记的枚举么,这个可以初步实现
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: BizExceptionEnum
* @Package: com.amos.bizexception.exception.type
* @author: zhuqb
* @Description: 自定义异常的编码和消息
* <p/>
* 这里没有实现setter方法,主要是为了不能修改枚举中的code值,
* 但是我们自定义了 definedMsg 方法,主要是为了方便用户自定义异常的信息
* 默认是选择枚举的msg信息
* @date: 2019/7/11 0011 上午 11:15
* @Version: V1.0
*/
@AllArgsConstructor
public enum BizExceptionEnum implements BizExceptionAssert {
/**
* 异常编码 1000
* 异常信息 对象不能为空
*/
NOT_NULL(1000, "对象不能为空");
private int code;
private String msg;
@Override
public int getCode() {
return this.code;
}
@Override
public String getMsg() {
return this.msg;
}
/**
* 此处可以动态修改msg的返回值
* 这么做是为了实现自定义异常信息
*
* @param msg
* @return
*/
public BizExceptionEnum definedMsg(String msg) {
this.msg = msg;
return this;
}
}
好了,至此我们对于自定义异常的捕捉和抛出就已经全部完结了,下面来看看我们业务端的调用吧
@Service
public class BizService {
void call(Object object) {
BizExceptionEnum.NOT_NULL.definedMsg("判断出对象为NULL,手动抛出异常").notNullAssert(object);
System.out.println("方法执行结束");
}
}
是不是发现很简单,一行代码就实现了手动抛出异常,逼格是不是瞬间高了?!!!
不仅仅如此,这个异常的封装还很容易进行扩展,如果想再次自定义新的异常,只需要继承BaseException
,提供实例化对象,并且提供自定义的Assert
来实现自定异常的初始化方法,最后提供对应的异常编码和信息的枚举来当做自定义异常实现类即可。
类的模型图如下:
全局异常的拦截处理
上面我们已经将异常的判断和抛出这个大头已经全部处理完毕了,接下来,得助于Spring提供的@ControllerAdvice
和@ExceptionHandler
,我们可以和方便的实现异常的全局拦截处理
/**
* Copyright © 2018 五月工作室. All rights reserved.
*
* @Project: biz-exception
* @ClassName: GlobalExceptionHandler
* @Package: com.amos.bizexception.advice
* @author: zhuqb
* @Description: 全局异常处理类
* <p/>
* 该Handler主要处理常见的web访问的信息
* @date: 2019/7/11 0011 上午 11:46
* @Version: V1.0
*/
@Slf4j
@ControllerAdvice(basePackages = {"com.amos.bizexception"})
@ResponseBody
public class GlobalExceptionHandler {
/**
* 处理自定义异常
*
* @param exception
* @return
*/
@ExceptionHandler(BaseException.class)
public Result handleBizException(BaseException exception) {
log.error("进入自定义异常拦截中...异常信息为:{}", exception.getMessage());
return ResultWapper.error(exception.getMessage());
}
}
这里的拦截了BaseException
这个基类,就是为了拦截所有的自定义异常.
至此,我们的全局代码异常处理封装就已经结束了,具体的代码可以查看: