SpringBoot 统一异常处理

2024/1/22 SpringBoot 框架

# 1、异常问题

  • 如果不统一处理异常,项目中将会需要大量的try catch异常处理,会使得代码逻辑复杂度增加,且代码可读性降低。
  • 异常的返回结果也会种类繁多,不符合我们统一接口封装的要求。

# 2、统一异常处理案例

# 2.1、自定义异常

一些业务上操作的问题,可以手动抛出自定义异常处理,返回错误信息给用户查看。

/**
 * @ClassName BusinessException
 * @Description 自定义业务异常
 * @Date 2024/1/22 15:54
 */
@Getter
@NoArgsConstructor
public class BusinessException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    /**
     * 响应码
     */
    private String status = BusinessResponseStatus.HTTP_STATUS_400.getResponseCode();

    /**
     * 结果信息
     */
    private String message = BusinessResponseStatus.HTTP_STATUS_400.getDescription();

    public BusinessException(String message) {
        this.message = message;
    }

    public BusinessException(String message,String status) {
        this.message = message;
        this.status = status;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 2.2、全局异常处理

通过@ControllerAdvice注解,配合@ExceptionHandler进行全局异常处理。

/**
 * @ClassName GlobalExceptionHandler
 * @Description 全局异常处理
 * @Date 2024/1/22 16:05
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 错误请求的异常处理程序
     * @return ResponseResult
     */
    @ResponseBody
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)  
    @ExceptionHandler(value = { BindException.class, ValidationException.class, MethodArgumentNotValidException.class })
    public ResponseResult<ExceptionData> handleParameterVerificationException(@NonNull Exception e) {
        ExceptionData.ExceptionDataBuilder exceptionDataBuilder = ExceptionData.builder();
        log.warn("Exception: {}", e.getMessage());
        if (e instanceof BindException) {
            BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
            bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage)
                    .forEach(exceptionDataBuilder::error);
        } else if (e instanceof ConstraintViolationException) {
            if (e.getMessage() != null) {
                exceptionDataBuilder.error(e.getMessage());
            }
        } else {
            exceptionDataBuilder.error("无效参数");
        }
        return ResponseResult.fail(exceptionDataBuilder.build(), "无效参数");
    }


    /**
     * 自定义异常
     * @param businessException
     * @return
     */
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    @ExceptionHandler(BusinessException.class)
    public ResponseResult<String> processBusinessException(BusinessException businessException) {
        return ResponseResult.fail(businessException.getMessage());
    }

    /**
     * 其他所有异常
     * @param exception
     * @return
     */
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public ResponseResult<String> processException(Exception exception) {
        return ResponseResult.fail(exception.getLocalizedMessage(), BusinessResponseStatus.FAIL.getDescription());
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 2.3、测试使用

  • 参数异常测试

当异常为这几类时参数异常:BindException.class, ValidationException.class, MethodArgumentNotValidException.class

@Email(message = "{email.is.not.valid}")
private String email;
1
2

测试结果

  • 自定义异常测试
@ResponseResultApi
@PostMapping("/add")
public Boolean addStudent(@Valid @RequestBody StudentDTO student){
    StudentPO studentPO = new StudentPO();
    BeanUtils.copyProperties(student,studentPO);
    if(Boolean.TRUE){
        throw new BusinessException("计算错误");
    }
    return studentService.save(studentPO);
}
1
2
3
4
5
6
7
8
9
10

测试结果

  • 其他异常测试

其他我们没法预测的异常,统一报服务器异常即可

@ResponseResultApi
@PostMapping("/add")
public Boolean addStudent(@Valid @RequestBody StudentDTO student){
    if(Boolean.TRUE){
        int i = 1/0;
    }
    return Boolean.TRUE;
}
1
2
3
4
5
6
7
8

测试结果

# 3、@ControllerAdvice

  • @ControllerAdvice是Spring3.2提供的新注解,它是一个Controller增强器,可对Controller进行增强处理。
  • @ControllerAdvice本质上还是当成一个Component组件注解,会被Spring扫描并注册到IOC容器中。
  • @ControllerAdvice可以说是AOP思想的实现,意为全局拦截,根据拦截规则对请求进行拦截,并交给@ExceptionHandler@InitBinder@ModelAttribute注解的方法处理。
  • 拦截规则

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 {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

指定包拦截,不指定时全部拦截

// 指定包拦截
@ControllerAdvice("com.xygalaxy.controller")
// basePackages指定包拦截
@ControllerAdvice(basePackages="com.xygalaxy.controller")
// 指定多个包拦截
@ControllerAdvice(basePackages={"com.xygalaxy.controller", "com.xy.controller"})
1
2
3
4
5
6
  • 搭配使用

  • @ControllerAdvice和@ExceptionHandler搭配进行全局异常处理。
  • @ControllerAdvice和@InitBinder搭配进行全局请求参数预处理。
  • @ControllerAdvice和@ModelAttribute搭配进行预设全局数据。

# 3.1、@ExceptionHandler

@ControllerAdvice和@ExceptionHandler搭配进行全局异常处理。 统一异常处理案例中已经演示,这里不再赘述。

# 3.2、@InitBinder

@ControllerAdvice和@InitBinder搭配进行全局请求参数预处理。

@RestControllerAdvice
public class GlobalHandler {
    /**
     * 全局日期格式处理为yyyy-MM-dd
     * @param dataBinder
     */
    @InitBinder
    public void handleInitBinder(WebDataBinder dataBinder){
        dataBinder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 3.3、@ModelAttribute

@ControllerAdvice和@ModelAttribute搭配进行预设全局数据。

@RestControllerAdvice
public class GlobalHandler {
    
    /**
     * 全局预测数据,用于在请求参数中直接使用
     */
    @ModelAttribute("currentUser")
    public UserDetails modelAttribute() {
        return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

// 控制层直接使用,通过@ModelAttribute读取
@PostMapping("saveSomething")
public String saveSomeObj(@ModelAttribute("currentUser") UserDetails operator) {
    // 保存操作,并设置当前操作人员的ID(从UserDetails中获得)
    return "success";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3.4、@RestControllerAdvice

@RestControllerAdvice注解包含了@ControllerAdvice注解和@ResponseBody注解。

# 4、@ControllerAdvice原理解析