本文分享一个使用于spring boot和spring web的全局异常处理机制.
本示例是用spring boot项目测试的
1.设计思想
整个异常处理机制,是针对于controller层进行的自动异常处理,如果是rpc的接口,则不适用.
整个机制的核心,是通过spring提供的controller增强来扩展处理异常
异常处理机制可以独立成单独的jar,只需要引入以后,加入一个注册的处理类,即可使用
可以处理系统异常和业务异常,也支持处理自定义异常
2.使用的技术
2.1 @RestControllerAdvice/@ControllerAdvice
@ControllerAdvice或@RestControllerAdvice 应用在类上, 分别扩展@Controller请求和@RestController请求,
其中@InitBinder可以在所有@RequestMapping注解的方法处理之前进行初始化处理
@ModelAttribute可以把值绑定到Model中
@ExceptionHandler用来处理全局异常
示例:
@ControllerAdvice
public class MyControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {}
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("","");
}
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Map errorHandler(Exception ex) {
Map map = new HashMap();
//...
return map;
}
}
2.2 @ExceptionHandler
@ExceptionHandler上面已经说了,可以处理全局异常,具体的用法如下:
@ExceptionHandler({Exception.class})
public JSONObject exception(Exception e){
JSONObject object = new JSONObject();
object.put("key",500);
object.put("value","系统异常");
return object;
}
3.代码实现
异常处理已一个单独的项目抽离出来,可以是spring boot项目,也可以是普通的spring web项目
3.1 项目结构
整个项目于的文件结构如下:
下面按不同的模块具体展示实现逻辑
3.2 自定义业务异常
此文件夹下主要是所有自定义的业务异常类,我示例了一个异常类,可以按照自己的需求定义一个或多个
示例代码如下:
@Data
public class BusinessException extends Exception {
//异常码
private Long code;
//异常信息
private String msg;
//异常提醒邮件地址(可选)
private String mailAdd = null;
//异常提醒邮件信息(可选)
private String mailBody;
public BusinessException(){
super();
this.code = 500L;
this.msg = "业务异常";
}
public BusinessException(Long code, String msg){
super(msg);
this.code = code;
this.msg = msg;
}
public BusinessException(ExEnum exEnum){
super(exEnum.getMsg());
this.code = exEnum.getCode();
this.msg = exEnum.getMsg();
}
public BusinessException(MainEnum mainEnum){
super();
this.code = 500L;
this.msg = "业务异常";
this.mailAdd = mainEnum.getMailAdd();
this.mailBody = mainEnum.getMessageBody();
}
}
异常类很简单,就是由异常码 异常信息和异常提醒邮件信息几个字段组成的; 又实现了几个常用的构造方法,已简化抛出异常时的代码.
此自定义异常有几个可优化点:
-
字段可以扩展增删
-
构造方法可以增删
当然,也支持在使用此异常处理机制的项目里自定义异常处理类
3.3 异常枚举
此枚举文件夹中,罗列使用到的异常类, 这里主要说明的是可以自定义异常枚举
自定义异常枚举,是项目预定义的常见异常,这样在需要排除异常类的时候,可以直接使用枚举抛出,不再需要new异常对象
这里演示了两种枚举,一种是普通的(ExEnum),一种是需要邮件提醒的(MailEnum),核心代码如下:
public enum ExEnum {
BASE_EX(5001L,"业务异常"),
//... 可以扩展异常类型
;
private Long code;
private String msg;
ExEnum(Long code, String msg){
this.code = code;
this.msg = msg;
}
//...getter setter
}
public enum MailEnum {
Business_1("zs@mail.com","系统发生异常","业务一负责人邮箱"),
//... 可以扩展异常类型
;
private String mailAdd;
private String messageBody;
private String msg;
MailEnum(String mailAdd, String messageBody, String msg){
this.mailAdd = mailAdd;
this.messageBody = messageBody;
this.msg = msg;
}
//...getter setter
}
3.4 异常处理程序
异常处理程序,是整个机制的核心,这里依据系统异常和自定义业务异常两个,分别由两个Handler处理.
核心的点就是使用@ExceptionHandler来处理不同的异常
示例代码如下:
3.4.1 系统异常处理
处理类自定义了许多default方法,处理常见的异常,也可以实现接口自己重写或新增
public interface BaseExceptionHandler {
Logger logger = LoggerFactory.getLogger(BaseExceptionHandler.class);
@ExceptionHandler({Exception.class})
default JSONObject exception(Exception e){
logger.error("exception-{}",e.getMessage(),e);
JSONObject object = new JSONObject();
object.put("key",500);
object.put("value","系统异常");
return object;
}
@ExceptionHandler({ArithmeticException.class})
default JSONObject arithmeticException(ArithmeticException ae){
logger.error("arithmeticException-{}",ae.getMessage(),ae);
JSONObject object = new JSONObject();
object.put("key",500);
object.put("value","算术异常" + ae.getMessage());
return object;
}
//TODO 可以扩展其他类型的异常
}
3.4.2 自定义业务异常处理
public interface BusinessExceptionHandler {
Logger logger = LoggerFactory.getLogger(BusinessExceptionHandler.class);
@ExceptionHandler({BusinessException.class})
default JSONObject businessException(BusinessException be){
logger.error("businessException-{}",be.getMessage(),be);
//需要执行异常邮件通知
if(be.getMailAdd() != null){
logger.info("模拟邮件发送动作,为{}发送邮件,内容为:{};{}", be.getMailAdd(), be.getMailBody(), be.getMessage());
}
JSONObject object = new JSONObject();
object.put("key",500);
object.put("value","异常:" + be.getMessage());
return object;
}
//TODO 可以定义其他类型的异常,并进行处理
}
3.5 自定义断言
自定义断言是为了减少代码中的if判断,使得代码更加关注业务逻辑的处理,这里只示例一个,用以展示思路,可以按照自己项目的需求, 扩展
示例如下
public class ExAssert {
/**
* 断言对象是否为空
* @param obj 被断言对象
* @param ex 指定异常枚举
* @throws BusinessException
*/
//@SneakyThrows是为了骗过编译器,避免直接throws异常,
//导致在使用断言的地方还需要手动抛出异常
@SneakyThrows
public static void isNotNull(Object obj, ExEnum ex) {
if(obj == null){
throw new BusinessException(ex);
}
}
/**
* 断言对象是否为空
* @param obj 被断言对象
* @param code 指定错误码
* @param msg 指定错误提示信息
* @throws BusinessException 业务异常
*/
@SneakyThrows
public static void isNotNull(Object obj, Long code, String msg) {
if(obj == null){
throw new BusinessException(code, msg);
}
}
/**
* 断言对象是否为空
* @param obj 被断言对象
* @param code 指定错误码
* @param msg 指定错误提示信息
* @throws BusinessException 业务异常
*/
@SneakyThrows
public static void isNotNull(Object obj, Long code, String msg, MainEnum mainEnum) {
if(obj == null){
throw new BusinessException(code, msg, mainEnum);
}
}
//TODO 可以增加其他断言
}
4.异常处理使用示例
4.1 pom文件添加依赖
和普通的pom文件添加依赖的方法一致
<dependency>
<groupId>jin.common</groupId>
<artifactId>exception</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
4.2 新增一个Handler装配异常处理程序
此类用来装配common包中提供的全局异常处理机制,需要哪几个装配哪几个
使用此Handler装配以后,此项目中所有的controller都会使用该全局异常处理机制了
@RestControllerAdvice
public class DemoBaseExceptionHandler implements BaseExceptionHandler, BusinessExceptionHandler {
//此类默认不需要实现,如果异常处理项目已满足,则不要实现
}
4.3 自定义异常处理\覆盖异常处理
@RestControllerAdvice
public class DemoBaseExceptionHandler implements BaseExceptionHandler, BusinessExceptionHandler {
//此类默认不需要实现,不过可以自定义异常处理机制
//此操作会覆盖接口默认实现的处理逻辑
@ExceptionHandler({ArithmeticException.class})
public JSONObject arithmeticException(ArithmeticException ae) {
JSONObject object = new JSONObject();
object.put("key", 5008);
object.put("value", "算术异常" + ae.getMessage());
return object;
}
}
4.4 使用实例代码
4.4.1 系统异常
@GetMapping("/demo")
public String demo(){
//模拟异常
int i = 0;
i = 1/i;
return "done" + System.currentTimeMillis();
}
效果:
4.4.2 业务异常
public void demo1(Object obj) throws BusinessException {
//模拟业务异常
throw new BusinessException(ExEnum.BASE_EX.getCode(),ExEnum.BASE_EX.getMsg());
}
效果:
4.4.3 断言
@Override
public void demo() {
//此代码模拟业务代码抛出异常
ExAssert.isNotNull(null, ExEnum.BASE_EX);
}
此机制可以处理大部分的异常,然而404等异常,无法处理,可以使用重新定向404页面的方法处理