传统的Java程序用try-catch语句来捕捉异常,但是,一旦漏掉一个异常,那整个程序就game-over,而SpringBoot项目采用了全局异常的概念-所有方法均可将异常抛出,并且专门安排一个类统一拦截并处理这些异常。
10.1 拦截异常
一旦项目项目开发上线,就必须对所有异常进行拦截,包括404/500这些最底层的异常,于是需要有拦截特定异常和拦截全局最底层异常。
10.1.1拦截特定异常
为了拦截异常,SpringBoot提供了两个注解,即@ControllerAdvice注解和@ExceptionHandler注解。
@ControllerAdvice注解用于标注类,把被@ControllerAdvice标注的类乘坐全局异常处理类;
@ExceptionHandler注解用于标注方法,把被@ExceptionHandler注解标注的方法用于处理特定异常。
具体形式如下:
@ControllerAdvice//标识为全局异常处理类
public class MyExceptionHandler {//拦截特定异常的方法,拦截的类必须继承自Exception@ExceptionHandler(ArrayIndexOutOfBoundsException.class)@ResponseBody//使用这个标识是为了在http请求时将处理结果返回到客户端的页面上public String handleControllerException() {return "拦截1个异常了哟";}//拦截多种异常,需要用{}包括起来@ExceptionHandler({NullPointerException.class, NumberFormatException.class})@ResponseBodypublic String handlerTwoExeption() {return "拦截2个异常";}
}
10.1.2拦截全局最底层异常
除了拦截自己已知的或者特意生成的异常类,SpringBoot项目还需要能拦截自己感觉不可能出现但实际却阴差阳错出现的异常,这就是要在项目中拦截所有异常的老祖宗Exception,这个Exception异常类就是最底层异常,需要一个全局类来处理此异常,一般都会创建一个GlobalExceptionHandler类来进行拦截。
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(NullPointerException.class)public String nullPointerExceptionHandler() {return "出现空指针了,这是我特意捕捉的";}@ExceptionHandler(Exception.class)public String exceptionHandler() {return "出现我没想到的异常了,我是来兜底的";}
}
10.3缩小拦截的范围
被@ControllerAdvice注解标注的类会默认拦截所有被触发的异常,我们也可以通过@ControllerAdvice注解的属性值,能够缩小拦截异常的范围。
10.3.1 拦截由某个或者多个包触发的异常
上面拦截的专门的异常类,也可以拦截包下面的类下面的异常
@ControllerAdvice("com.sun01.test01.controller")//只拦截控制器类包里类发生的异常,如果有多个包,直接({xx,yy})
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public String exceptionHandler() {return "出现我没想到的异常了,我是来兜底的";}
}
10.3.2拦截由某个或者多个注解标注的类触发的异常
如果一个SpringBoot项目的所有控制器类被存储在一个包下,那么通过设置@ControllerAdvice注解的annotations属性(annotation的中文意思就是注解),既能够指定拦截由某个注解标注的类触发的异常,又能拦截指定由多个注解标注的类触发的异常。
总结起来就是不用annotations属性,拦截的就是包下面的异常,如果使用了annotations属性,那么拦截的就是使用注解标注过的类的异常。
@ControllerAdvice(annotations = RestController.class)//只拦截@RestController标注的控制器类发生的异常,如果有多个包,直接({xx,yy})
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public String exceptionHandler() {return "出现我没想到的异常了,我是来兜底的";}
}
10.4 拦截自定义异常
全局异常处理类同样支持拦截自定义异常,想要拦截自定义异常,须执行3个步骤。
- 创建自定义异常类,这个须继承RuntimeException运行时异常类,并重新父类的构造方法
- 创建全局异常处理类,用于拦截自定义的异常。
- 在控制器类中指定自定义异常触发条件。
以一个经典案例中的异常类为例。
1,创建运行时异常类
public class ImoocMallException extends RuntimeException {private final Integer code;private final String message;public ImoocMallException(Integer code, String message) {this.code = code;this.message = message;}public ImoocMallException(ImoocMallExceptionEnum exceptionEnum) {this(exceptionEnum.getCode(), exceptionEnum.getMsg());}public Integer getCode() {return code;}@Overridepublic String getMessage() {return message;}
}
code是给前端标识的,message是给前端展示的,为了方便管理,使用一个异常枚举来标识和管理各类不同异常。
public enum ImoocMallExceptionEnum {NEED_USER_NAME(10001, "用户名不能为空"),NEED_PASSWORD(10002, "密码不能为空"),PASSWORD_TOO_SHORT(10003, "密码长度不能小于8位"),NAME_EXISTED(10004, "不允许重名"),INSERT_FAILED(10005, "插入失败,请重试"),WRONG_PASSWORD(10006, "密码错误"),NEED_LOGIN(10007, "用户未登录")...........}
2,创建全局异常处理类
@ControllerAdvice
public class GlobalExceptionHandler {private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);@ExceptionHandler(Exception.class)@ResponseBodypublic Object handleException(Exception e) {log.error("Default Exception: ", e);return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);//返回系统异常的错误提醒}@ExceptionHandler(ImoocMallException.class)@ResponseBodypublic Object handleImoocMallException(ImoocMallException e) {log.error("ImoocMallException: ", e);return ApiRestResponse.error(e.getCode(), e.getMessage());//创建固定json格式的返回数据模板}..........}
3,在控制器类或者其他类触发自定义异常
@GetMapping("/login")
@ResponseBody
public ApiRestResponse login(@RequestParam("userName") String userName,@RequestParam("password") String password, HttpSession session)throws ImoocMallException {if (StringUtils.isEmpty(userName)) {return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_USER_NAME);这一步就是根据异常类型返回固定格式的json数据,这一步没有异常,也是正常的请求返回}if (StringUtils.isEmpty(password)) {return ApiRestResponse.error(ImoocMallExceptionEnum.NEED_PASSWORD);//因为error()是返回统一的ApiRestResponse对象,也可以通过触发异常来//throw new ImoocMallException(ImoocMallExceptionEnum.NEED_PASSWORD);//触发自定义异常,和上面的语句是一样的作用,但上面一步到位,所以屏蔽触发异常}User user = userService.login(userName, password);//如果密码错误,直接在Service层抛异常走了//保存用户信息时,不保存密码user.setPassword(null);//到这一步了,说明登录就成功了session.setAttribute(Constant.IMOOC_MALL_USER, user);return ApiRestResponse.success(user);
}
在Service层遇到的问题直接抛出异常
@Override
public User login(String userName, String password) throws ImoocMallException {String md5Password = null;try {md5Password = MD5Utils.getMD5Str(password);} catch (NoSuchAlgorithmException e) {e.printStackTrace();}User user = userMapper.selectLogin(userName, md5Password);if (user == null) {throw new ImoocMallException(ImoocMallExceptionEnum.WRONG_PASSWORD);//密码错误导致的查询问题直接当做异常抛出}return user;
}
我第一次看到这个处理过程时我是很诧异的,后面觉得这样的设计真是太妙了,控制器层常常需要与Service有一次或者多次异常,一旦Service层需要判断或者问题往往无法通过返回的数据来进行说明,如上面的用户登录的Service层,登录返回用户,一旦密码错误,你不能返回正式的用户信息,因为你密码错误没有查到,你也不能返回null,因为用户实体是存在的,而直接抛出自定义的异常,并将异常说明给予说明返回给用户客户端上,问题就直接解决了。这就是处理全局异常最好的使用。
打完收工。