全局异常捕获
什么是异常?程序在启动或者运行时没有按照预期的执行,在执行途中发生某种未知的错误,导致程序非正常停止或者报错。
在我们的程序中,肯定会伴随着很多的异常,启动时:空对象、找不到数据库、用户名密码不对等等异常,都会在程序启动时抛出异常信息,运行时:空引用、参数不匹配等等都会在程序运行时抛出异常,启动的时候抛出异常我们可以马上修改,但是程序正在运行的突然报了一个错,如果没有对这个错误做处理,用户可能会看到一堆的代码信息,很不友好,所以今天讲解一下springboot全局异常捕获。
我们先看一个程序没有做异常处理会发生什么事情,下面是一个小小的例子:
private static List list = new ArrayList(); static { list.add("小明"); list.add("小红"); list.add(null); } /*** 测试* @return*/ @GetMapping(value = "/test") public String test(){ for(String s: list){ if(s.equals("小红")){log.info("听说点赞的都发财了!!!"); }else{log.info("没点赞的好像也发财了!!!"); } } return "success"; }
这是一个典型的空指针异常,真正写代码的时候是不会这么干的,这里为了展示效果才这样写的,我们请求/test接口,看看会发生什么?
我靠?这是什么玩意?这要是让用户/甲方爸爸看到,那还不得被骂死?那如何解决这个问题呢?
我们将for循环这段代码加try/catch异常捕获处理。
改造后的代码:
/*** 测试* @return*/ @GetMapping(value = "/test") public String test(){ try{ for(String s: list){if(s.equals("小红")){log.info("听说点赞的都发财了!!!");}else{log.info("没点赞的好像也发财了!!!");} } }catch (Exception e){ return "网络繁忙,请稍后再试"; } return "success"; }
再次访问:
发现已经不是提示代码信息了,而是提示了比较友好的网络繁忙,那这个时候,你可能就会有问题了,那我岂不是需要在每个接口请求中添加一个try/catch异常捕获?这样不仅让代码的可读性变差,还会牵扯到代码的可维护性,以后接手代码的同事心里可能是崩溃的。
java有没有一种统一的处理方式呢?让特定的错误返回特定的提示,java这么强大,怎么可能没有?下面我们来讲讲如何实现springboot的全局异常捕获。
我们先定义一个全局异常捕获类:GlobalExceptionRespone,由于Exception是大部分异常的大哥大,所以我们针对Exception做一个异常处理
package com.ymy.exceptions;import com.ymy.utils.ConstantUtil;import com.ymy.vo.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j@RestControllerAdvicepublic class GlobalExceptionRespone { /*** 全局异常捕获* @param e* @return*/ @ExceptionHandler(value = Exception.class) public Object errorHandler( Exception e) { log.error("网络繁忙,请稍后再试 ->",e); return Result.faild(ConstantUtil.ResponeCode.SYS_ERROR_CODE,ConstantUtil.ResponeCode.SYS_ERROR); } }
这个时候我们去掉业务代码中的try/catch:
@GetMapping(value = "/test") public String test() { for (String s : list) { if (s.equals("小红")) {log.info("听说点赞的都发财了!!!"); } else {log.info("没点赞的好像也发财了!!!"); } } return "success"; }
重启,运行,结果如下:
发现了什么?程序发现了空指针,但是并没有像最开始那样抛出乱信息,而是经过我们处理过的信息,这感觉是不是不要太爽,就增加一个类,就解决了程序中需要假如try/catch的苦恼。
细心的你可能发现我在全局异常捕获类中添加了一个注解:@RestControllerAdvice,这个注解有着怎样的魅力呢?
@RestControllerAdvice是一个组合注解:@ControllerAdvice+@ResponseBody
spring4.3之后被引入的一个注解,它可以被使用到HandlerMapping中,通过@ExceptionHandler来处理HandlerMapping中的异常信息。
@ExceptionHandler通过指定特定的异常类对象来做出对应的处理,如果没有指定到特定的异常类那么他将找到与这个异常类最接近的类,比如之前的空指针异常,我们并没有针对空指针做一个全局异常处理,所以@ExceptionHandler找到了他的大哥:@Exception,如果我们配置一下空指针的异常捕获,会出现什么样的结果呢?
在全局异常捕获类中加入:
/*** 全局异常捕获* @param e* @return*/ @ExceptionHandler(value = NullPointerException.class) public Object nullPointerException( Exception e) { log.error("程序中出现空引用,请检查! ->",e); return Result.faild(ConstantUtil.ResponeCode.NULL_POINTER_EXCEPTION_CODE,ConstantUtil.ResponeCode.NULL_POINTER_EXCEPTION); }
启动程序:
是不是像我刚刚说所的那样,@ExceptionHandler会优先寻找报错的异常对象,找到了直接返回,没有找到,继续往后找。
在这里我展示几种常见的异常类:
1.MissingServletRequestParameterException:缺少参数子类。
2.HttpMessageNotReadableException:参数解析失败。
3.MethodArgumentNotValidException:当对用@Valid注释的参数进行验证失败时,将引发异常。
4.BindException:参数绑定错误并且是致命错误的时候。
5.ConstraintViolationException:违反约束条件。
6.ValidationException:基础异常中所有bean验证异常的问题。
7.NoHandlerFoundException:默认情况下,当DispatcherServlet找不到请求的处理程序时,它将发送404响应。但是,如果将其属性“ throwExceptionIfNoHandlerFound” *设置为 true,则会引发此异常,并且可以使用配置的HandlerExceptionResolver进行处理。
8.HttpRequestMethodNotSupportedException:不支持的请求方式
9.HttpMediaTypeNotSupportedException:当客户端发布、放置或请求处理程序不支持的类型。
10.DuplicateKeyException:当试图插入或更新数据导致违反主键或惟一约束时引发异常。
自定义异常
需求:删除用户信息,包含三张表:用户基础信息、用户详细信息、用户图像表。
假设删除用户基础信息返回1(成功)、删除详细信息返回1(成功)、删除图像信息返回0(失败)。
如果我们将这三个放到一个事物里面,事务肯定是会提交的,因为程序没有发生异常,只是再删除的时候没有返回1而已,所以这时候我们就需要自己创建一个异常,让程序抛出。
现在我们就用一个自定义的异常来解决这个问题。
第一步:创建一个自定义的异常类继承RuntimeException(运行时异常)。
package com.ymy.exceptions;import lombok.Getter;@Getterpublic class MyException extends RuntimeException { private static final long serialVersionUID = 1L; /*** 状态码*/ private String code; /*** 提示消息*/ private String msg; public MyException(String code,String msg) { this.code = code; this.msg = msg; }}
第二步:在全局异常类(GlobalExceptionRespone.java)中加入自定义的异常捕获:
/*** 业务层需要自己声明异常的情况*/ @ExceptionHandler(MyException.class) public Object handleMyTokenExcption(MyException e) { log.error("->",e); return Result.faild(e.getCode(),e.getMsg()); }
第三步,再返回0的时候调用自定义的异常:
/*** 修啊给i用户信息** @return*/ @RequestMapping(value = "/updateUser", method = RequestMethod.POST) public Result addUser() { int count = 1; log.info("第一步:删除基础信息。。。。。。。。。。。"); log.info("第二步:删除详细信息。。。。。。。。。。。"); log.info("第三步:删除图像信息。。。。。。。。。。。,结果返回了0,表示删除失败"); count = 0; if(count == 0){ throw new MyException("delete_excption","删除图像信息失败,请过一会在尝试"); } return Result.OK(); }
这里就不具体去实现了,主要是看如何调用自定义的异常使用,调用updateUser接口:
程序抛出了异常,这就会导致事务不会提交,是我们的预期结果,我们再来看看返回给用户的信息:
提示消息正是我在删除失败之后调用自定义异常。
@Valid
相信很多人都是用过:@Valid,他是用来校验参数的一个重要工具,他能简化我们很多的if代码语句,下面我们来介绍一下他的使用方法。
需求,添加一个用户,需要添加:用户名、手机、邮箱、年龄,并且字段都不可为空,邮箱格式需要正确,年龄必须在0-120岁之间。
正常的写法是不是需要在controller中添加一堆的if语句,来判断传递的参数是否符合要求,今天我们讲解一种可读性更好,代码更简单的方式:@Valid注解。
用户VO类:
package com.ymy.vo;import lombok.Getter;import lombok.Setter;import javax.validation.constraints.*;@Getter@Setterpublic class UserVo { @NotEmpty(message = "用户名不能为空") private String userName; @NotEmpty(message = "手机号不能为空") private String phone; @NotEmpty(message = "邮箱不可为空") @Email(message = "邮箱格式不正确") private String email; @Min(0) @Max(120) @NotNull(message = "年龄必填") private Integer age;}
controller接口实现:
* 添加用户** @return*/ @RequestMapping(value = "addUser", method = RequestMethod.POST) public Result addUser(@RequestBody @Valid UserVo userVo, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return Result.faild(ConstantUtil.ResponeCode.INVALID_PARAMETER,bindingResult.getFieldError().getDefaultMessage()); } log.info("添加成功。。。。。。{}", userVo); return Result.OK(); }
运行addUser接口:
发现已经实现了参数校验的功能,但是代码看着还是有一点点的不爽,那是为什么呢?因为controller中还是有着一个if的判断条件,如果能把这个if判断也去掉,那整个世界都美好了。
刚刚在讲全局异常捕获的时候提到了一个异常类,:MethodArgumentNotValidException,不知道还记得吗,他就是配合我们的@Valid使用的,具体如何使用呢?
在全局异常捕获类(GlobalExceptionRespone.java)中添加对@Valid注解参数校验失败捕获:
/*** 校验错误拦截处理** @param exception 错误信息集合* @return 错误信息*/ @ExceptionHandler(MethodArgumentNotValidException.class) public Object validationBodyException(MethodArgumentNotValidException exception){ BindingResult bindingResult = exception.getBindingResult(); if (bindingResult.hasErrors()) { return Result.faild(ConstantUtil.ResponeCode.INVALID_PARAMETER,bindingResult.getFieldError().getDefaultMessage()); } return Result.faild(ConstantUtil.ResponeCode.SYS_ERROR_CODE); }
就是这么简单,我们现在去掉controller中的if判断:
/*** 添加用户** @return*/ @RequestMapping(value = "addUser", method = RequestMethod.POST) public Result addUser(@RequestBody @Valid UserVo userVo) { log.info("添加成功。。。。。。{}", userVo); return Result.OK(); }
运行结果如下:
完美,从一堆的if判断语句到现在的一个注解,代码的可读性以及可维护性都得到了很大的提升,现在的心情就是倍爽
@Valid常用校验注解:
@Null:只能为空
@NotNull:不能为空
@Max(value):最大数字
@Min(value):最小数字
@Size(max,min):字符长度必须在min到max之间
@DecimalMax(value):不大于指定数字
@DecimalMin(value):不小于指定数字
@Pattern(value):正则表达式
@Digits(integer,fraction):小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Past:日期类型:输入时间比当前时间早
@Future:将来日期
@Past:过去日期
@Email:验证邮箱格式
@AssertFalse:只能为false
@AssertTrue:只能为true
@NotEmpty:参数不能为空
@NotBlank:参数不能为空
依赖类:
package com.ymy.vo;import com.ymy.utils.ConstantUtil;import com.ymy.utils.DateUtil;import lombok.Getter;@Getterpublic class Result { private String code; private String msg; private T data; private String createTime = DateUtil.getdateNow(); private Result(String code){ this.code = code; } private Result(String code,T data){ this.code = code; this.data = data; } private Result(String code,String msg){ this.code = code; this.msg = msg; } private Result(String code,String msg,T data){ this.code = code; this.msg = msg; this.data = data; } /*** 默认成功返回* @param* @return*/ public static Result OK(){ return new Result(ConstantUtil.ResponeCode.SUCCESS_CODE); } /*** 返回带code的信息* @param code* @param* @return*/ public static Result OK(String code){ return new Result(code); } /*** 返回只带code的信息* @param code* @param* @return*/ public static Result faild(String code){ return new Result(code,ConstantUtil.ResponeCode.SYS_ERROR); } /*** 返回带code与提示消息的对象* @param code* @param msg* @param* @return*/ public static Result faild(String code,String msg){ return new Result(code,msg); }}
package com.ymy.utils;/** * 常量字典 */public class ConstantUtil { /*** 返回状态码信息*/ public static class ResponeCode{ /** * 正常 */ public static final String SUCCESS_CODE="suc-001"; /** * 系统错误code */ public static final String SYS_ERROR_CODE="sys_error"; /** * 系统错误code */ public static final String NULL_POINTER_EXCEPTION_CODE="Null_Pointer_Exception"; /** * 系统错误code */ public static final String NULL_POINTER_EXCEPTION="程序中出现空引用,请检查!"; /** * 系统错误 */ public static final String SYS_ERROR="网络繁忙,请稍后再试"; /** * 参数错误code */ public static final String INVALID_PARAMETER="invalid_parameter"; } /*** 日期格式*/ public static class Date{ public static final String DATE_FORMATE="yyy-MM-dd HH:mm:ss"; }}
项目地址:/yq0601/global-exceptions.git