愿你如阳光,明媚不忧伤。
目録
- 1. 全局异常处理
- 2. 定义返回的统一 json 结构
- 3. 处理系统异常
- 3.1 处理参数缺失异常
- 3.2 处理空指针异常
- 3.3 拦截 Exception
- 4. 拦截自定义异常
- 4.1 定义异常信息
- 4.2 拦截自定义异常
1. 全局异常处理
全局异常( GlobalException)在项目开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。如果对每个过程都单独作异常处理,那系统的代码耦合度会变得很高,此外,开发工作量也会加大而且不好统一,这也增加了代码的维护成本。
针对这种实际情况,我们需要将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能单一,也实现了异常信息的统一处理和维护。同时,我们也不希望直接把异常抛给用户,应该对异常进行处理,对错误信息进行封装,然后返回一个友好的信息给用户。
2. 定义返回的统一 json 结构
此部分略,前课程有发布 → 【SpringBoot 教程】Spring Boot 返回 Json 数据及数据封装
3. 处理系统异常
新建一个 GlobalExceptionHandler 全局异常处理类,然后加上 @ControllerAdvice 注解即可拦截项目中抛出的异常。
- GlobalExceptionHandler.java
使用的时候,在方法上加 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,最后将结果通过统一的 json 结构体返回给调用者。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler extends LogBean {
}
点开 @ControllerAdvice 注解可以看到, @ControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。除此之外,该注解还有个basePackages 属性,该属性是用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。
@ResponseBody 注解是为了异常处理完之后给调用方输出一个 json 格式的封装数据。
- @ControllerAdvice
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
3.1 处理参数缺失异常
POST请求需要携带一些参数,但是往往有时候参数会漏掉。此时我们需要定义一个处理参数缺失异常的方法,来给前端或者调用方提示一个友好信息。
- GlobalExceptionHandler.java
参数缺失的时候,会抛出 HttpMessageNotReadableException ,我们可以拦截该异常,做一个友好处理。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler extends LogBean {
/**
* 缺少请求参数异常
*
* @param ex
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(MissingServletRequestParameterException ex) {
log.error("缺少请求参数,{}", ex.getMessage());
return new JsonResult("400", "缺少必要的请求参数");
}
}
我们来写个简单的 Controller 测试一下该异常,通过 POST 请求方式接收两个参数:姓名和密码。
- ExceptionController .java
@RestController
@RequestMapping("/exception")
public class ExceptionController {
private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
@PostMapping("/test")
public JsonResult test(@RequestParam("name") String name, @RequestParam("pass") String pass) {
logger.info("name:{}", name);
logger.info("pass:{}", pass);
return new JsonResult();
}
}
然后使用 Postman 来调用一下该接口,调用的时候,只传姓名,不传密码,就会抛缺少参数异常,该异常被捕获之后,就会进入我们写好的逻辑。
3.2 处理空指针异常
从数据库中查询数据的时候,不管是查询一条记录封装在某个对象中,还是查询多条记录封装在一个 List 中,我们接下来都要去处理数据,那么就有可能出现空指针异常,因为谁也不能保证从数据库中查出来的东西就一定不为空,所以在使用数据时一定要先做非空判断。
- GlobalExceptionHandler.java
空指针的时候,会抛出 NullPointerException ,我们可以拦截该异常,做一个友好处理。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler extends LogBean {
/**
* 空指针异常
*
* @param ex
* @return
*/
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleTypeMismatchException(NullPointerException ex) {
System.err.println("空指针异常" + ex.getMessage());
log.error("空指针异常,{}", ex.getMessage());
return new JsonResult("500", "空指针异常");
}
}
3.3 拦截 Exception
异常很多,比如还有 RuntimeException,数据库还有一些查询或者操作异常等等。由于 Exception 异常是父类,所有异常都会继承该异常,所以我们可以直接拦截 Exception 异常,一劳永逸。
- GlobalExceptionHandler.java
拦截 Exception 虽然可以一劳永逸,但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下面,如果都没有找到,最后再拦截一下 Exception 异常,保证输出信息友好。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler extends LogBean {
/**
* 系统异常 预期以外异常
*
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
log.error("系统异常:", ex);
return new JsonResult("500", "系统发生异常,请联系管理员");
}
}
4. 拦截自定义异常
在实际项目中,除了拦截一些系统异常外,在某些业务上,我们还需要自定义一些业务异常,比如在微服务中,服务之间的相互调用。要处理一个服务的调用时,可能会调用失败或者调用超时等等,此时我们需要自定义一个异常,当调用失败时抛出该异常,给 GlobalExceptionHandler 去捕获。
4.1 定义异常信息
由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。
- BusinessMsgEnum
/**
* 业务异常提示信息枚举类
*
* @author : ISCCF_A
* @version : ITGodRoad_0.0.1
* @dateTime: 2021/10/15 11:03
* @since : 11
*/
public enum BusinessMsgEnum {
/**
* 参数异常
*/
PARMETER_EXCEPTION("102", "参数异常!"),
/**
* 等待超时
*/
SERVICE_TIME_OUT("103", "服务调用超时!"),
/**
* 参数过大
*/
PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),
/**
* 500 : 一劳永逸的提示也可以在这定义
*/
UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
// 错误码
private String code;
// 错误消息
private String msg;
private BusinessMsgEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
// 省略get和set
}
4.2 拦截自定义异常
然后我们可以定义一个业务异常类,当出现业务异常时,我们就抛这个自定义的业务异常即可。
- GlobalExceptionHandler.java
在构造方法中,传入我们上面自定义的异常枚举类,在项目中如果有新的异常信息需要添加,直接在枚举类中添加即可。
public class BusinessErrorException extends RuntimeException {
private static final long serialVersionUID = -7480022450501760611L;
/*** 异常码 */
private String code;
/*** 异常提示信息 */
private String message;
public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
this.code = businessMsgEnum.getCode();
this.message = businessMsgEnum.getMsg();
}
// 省略get和set
}
- GlobalExceptionHandler.java
业务代码中,模拟抛出业务异常,测试一下。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler extends LogBean {
@GetMapping("/business")
public JsonResult testException() {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
}
return new JsonResult();
}
}
使用 Postman 来调用一下该接口