springboot全局异常实现以及@Valid和@Validated优雅实现入参验证

6/23/2022

# 前序

# 为什么要有全局异常❓

  1. 统一异常处理:在开发过程中,可能会遇到多个地方抛出的不同类型的异常,如果没有统一的异常处理机制,就需要在每个可能发生异常的地方进行单独处理。这样会导致代码冗余,增加开发和维护的工作量。全局异常处理机制通过统一捕获和处理异常,避免了在各个地方重复编写相同的异常处理逻辑,提高了代码的可重用性和可维护性。
  2. 错误信息的友好展示:默认情况下,当发生异常时,Spring Boot会返回一些默认的错误信息,例如堆栈跟踪信息。这些信息对于用户来说可能不够友好,也不利于问题的定位和解决。通过全局异常处理机制,我们可以自定义异常的返回消息、状态码等,使错误信息更加友好和可读。这样用户在遇到异常时能够更好地理解问题,并根据错误信息采取相应的操作。
  3. 异常日志的记录和追踪:在应用程序中,异常日志的记录对于排查问题和定位Bug非常重要。全局异常处理机制可以在异常发生时记录异常日志,包括异常类型、异常信息、发生异常的位置等。通过记录异常日志,我们可以更方便地追踪异常的来源,并及时发现和解决潜在的问题。
  4. 统一错误处理逻辑:在应用程序中,可能存在一些通用的错误处理逻辑,例如处理未授权异常、处理业务异常等。通过全局异常处理机制,我们可以集中管理这些通用的错误处理逻辑,并根据不同的异常类型提供相应的处理方式。这样可以使应用程序的错误处理更加一致,减少了重复的代码和逻辑,提高了代码的可读性和可维护性。

# 什么是全局异常处理❓

全局异常处理是指捕获应用程序中的所有异常并提供统一的处理机制。无论是在控制器层、服务层还是数据访问层中抛出的异常,都可以通过全局异常处理进行捕获和处理,从而保证应用程序的稳定性和可靠性。

# 异常处理器的作用🌛

异常处理器是一个专门处理异常的组件,它可以根据异常的类型提供不同的响应方式。通过异常处理器,我们可以自定义异常的返回消息、状态码等,使错误信息更加友好和可读。

# 在实际开发中对于异常我们应该一直抛出吗❓

对于异常的处理,一般来说,我们应该根据具体的情况来决定是抛出异常还是捕获并处理异常。以下是一些指导原则:

  1. 检查异常 vs. 非检查异常: Java中的异常分为检查异常(Checked Exception)和非检查异常(Unchecked Exception)两种类型。检查异常是需要在代码中显式捕获或声明抛出的异常,而非检查异常则不需要。对于检查异常,我们通常应该在合适的地方捕获并处理异常,以避免编译错误。对于非检查异常,通常是由程序出现了无法恢复的错误或意外情况,建议将其抛出,让调用者或上层方法处理。
  2. 异常处理的层级: 在一个应用程序中,我们可以在不同的层级处理异常,例如在控制器层、服务层或数据访问层。通常来说,我们应该在能够完整理解并处理异常的层级进行异常处理。==如果在某一层级无法处理异常,可以将异常抛出给上层进行处理,直到能够处理异常或者达到最顶层的全局异常处理器。==
  3. 业务逻辑 vs. 系统错误: 在编写代码时,我们需要区分业务逻辑错误和系统错误。业务逻辑错误是指由于业务规则或输入数据错误而引起的异常情况,这种异常通常可以预见并进行相应的处理。而系统错误则是由于系统故障、网络中断或其他无法控制的原因而引起的异常,这种异常通常是无法预测和修复的。对于业务逻辑错误,我们应该捕获并处理异常,提供有意义的错误信息给用户或日志记录;==对于系统错误,通常应该将异常抛出并由上层进行处理。==
  4. 异常处理的粒度: 我们需要根据具体情况和业务需求来决定异常处理的粒度。有些异常可以在当前方法内捕获并处理,而有些异常可能需要在更高级别的调用者中处理。在处理异常时,要确保在异常处理的过程中不会丢失必要的上下文信息,并且能够提供合适的错误反馈给用户或系统管理员。

总的来说,异常应该根据情况进行适当的抛出和处理。对于可以预见并处理的异常,应该捕获并提供合适的错误处理逻辑;对于无法处理的异常或需要上层进行处理的异常,应该将其抛出,以便更高级别的代码能够采取适当的措施。

# 全局异常在springboot项目中的具体实现

# 自定义异常类的实现

package com.todoitbo.tallybookdasmart.exception;


import lombok.extern.slf4j.Slf4j;

/**
 * @author xiaobo
 */
@SuppressWarnings("unused")
@Slf4j
public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = -4879677283847539655L;

    private int errorCode;

    private String errorMessage;

    private String exceptionMessage;

    private Exception exception;

    public BusinessException(String errorMessage) {
        super(errorMessage);
        this.errorMessage = errorMessage;
    }

    public BusinessException(int errorCode, String errorMessage) {
        super(errorMessage);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

    public BusinessException(int errorCode, String errorMessage, Exception exception) {
        super(errorMessage);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
        this.exception = exception;
    }

    public BusinessException(String errorMessage, String exceptionMessage) {
        super(errorMessage);
        this.exceptionMessage = exceptionMessage;
        this.errorMessage = errorMessage;
    }

    public String getExceptionMessage() {
        return exceptionMessage;
    }

    public void setExceptionMessage(String exceptionMessage) {
        this.exceptionMessage = exceptionMessage;
    }

    public int getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public Exception getException() {
        return exception;
    }

    public void setException(Exception exception) {
        this.exception = exception;
    }

    public BusinessException(int errorCode, String errorMessage, String exceptionMessage) {
        super(errorMessage);
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
        this.exceptionMessage = exceptionMessage;
    }

    public BusinessException setCause(Throwable cause){
        this.initCause(cause);
        return this;
    }

    public BusinessException setLog(){
        log.error(errorMessage+exceptionMessage);
        return this;
    }
}

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

# 全局异常捕获配置类

package com.todoitbo.tallybookdasmart.exception;


import com.todoitbo.tallybookdasmart.dto.BoResult;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author xiaobo
 * <h1>全局异常捕获</h1>
 */
@RestControllerAdvice
@SuppressWarnings("unused")
public class GlobalExceptionHandler {

    /**
     * 捕捉UnauthorizedException自定义异常
     *
     * @return boResult
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(CustomUnauthorizedException.class)
    public BoResult handle401(CustomUnauthorizedException e) {
        return new BoResult(HttpStatus.UNAUTHORIZED.value(), "无权访问(Unauthorized):" + e.getMessage(), null);
    }

    /**
     * 捕捉校验异常(BindException)
     *
     * @return boResult
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public BoResult validException(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        Map<String, Object> result = this.getValidError(fieldErrors);
        return new BoResult(HttpStatus.BAD_REQUEST.value(), result.get("errorMsg").toString(), result.get("errorList"));
    }

    /**
     * 捕捉校验异常(MethodArgumentNotValidException)
     *
     * @return boResult
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public BoResult validException(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        Map<String, Object> result = this.getValidError(fieldErrors);
        return new BoResult(HttpStatus.BAD_REQUEST.value(), result.get("errorMsg").toString(), result.get("errorList"));
    }

    /**
     * 捕捉其他所有自定义异常
     *
     * @return boResult
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BusinessException.class)
    public BoResult handle(BusinessException e) {
        return new BoResult(HttpStatus.BAD_REQUEST.value(), e.getErrorMessage(), null);
//        return BoResult.defineError(e);
    }

    /**
     * 捕捉404异常
     *
     * @return boResult
     */
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoHandlerFoundException.class)
    public BoResult handle(NoHandlerFoundException e) {
        return new BoResult(HttpStatus.NOT_FOUND.value(), e.getMessage(), null);
    }

    /**
     * 捕捉其他所有异常
     *
     * @param request 请求
     * @param ex      异常
     * @return boResult
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public BoResult globalException(HttpServletRequest request, Throwable ex) {
        return new BoResult(this.getStatus(request).value(), ex.toString() + ": " + ex.getMessage(), null);
    }

    /**
     * 获取状态码
     *
     * @param request 请求
     * @return boResult
     */
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }

    /**
     * 获取校验错误信息
     *
     * @param fieldErrors 字段错误值
     * @return boResult
     */
    private Map<String, Object> getValidError(List<FieldError> fieldErrors) {
        Map<String, Object> result = new HashMap<>(16);
        List<String> errorList = new ArrayList<>();
        StringBuffer errorMsg = new StringBuffer("校验异常(ValidException):");
        for (FieldError error : fieldErrors) {
            errorList.add(error.getField() + "-" + error.getDefaultMessage());
            errorMsg.append(error.getField()).append("-").append(error.getDefaultMessage()).append(".");
        }
        result.put("errorList", errorList);
        result.put("errorMsg", errorMsg);
        return result;
    }


    /**
     * 单个参数验证.
     *
     * @param ex RuntimeException
     * @return String
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public BoResult validExceptionHandler(ConstraintViolationException ex) {
        return BoResult.resultFail(ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage)
                .collect(Collectors.joining("")));

    }
}
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

@RestControllerAdvice的作用是将一个类标记为全局异常处理器,并且能够捕获和处理控制器中抛出的异常。同时,它还能够将处理结果转换为JSON格式,并作为响应返回给客户端。

上面的全局异常捕获涵盖的包括装态码异常捕获比如400,以及自定义异常捕获,还有参数异常捕获,具体实现如下

# 效果展示

系统异常捕获

在这里插入图片描述

⚠️:对于这种异常虽然可以捕获但是却带给用户不好的体验,这是需要使用自定义异常,这里我直接try,catch了,一般是对与系统异常我们可以在全局异常中使用自定义异常,来返回message

自定义异常捕获

image-20230527175249432

🉑:这种自定义异常的话我们不仅可以给用户展示看的懂的,而且在日志中也可以打印出真正异常的原因

# @Validated和@Valid大法

# @Validated和@Valid是啥子东东❓

@Validated@Valid 是 Spring 框架中用于数据验证的注解,用于确保传入的数据满足特定的验证规则。

@Validated 注解是 Spring 框架中的验证注解,用于对方法参数进行验证。它可以应用于控制器的请求处理方法或服务类的方法上。使用 @Validated 注解时,你可以在方法参数上使用其他验证注解,如 @NotNull@Size@Pattern 等,以指定参数的验证规则。@Validated 注解还支持分组验证,可以在不同的场景下应用不同的验证规则。

示例使用 @Validated 的方法签名:

@PostMapping("/users")
public ResponseEntity createUser(@Validated(UserCreation.class) @RequestBody UserDto userDto) {
    // 处理创建用户的逻辑
}
1
2
3
4

@Valid 注解是 Java 标准库中的验证注解,用于对对象的属性进行验证。它可以应用于实体类的属性上,表示需要对该属性进行验证。当使用 @Valid 注解时,你可以在属性上使用其他验证注解,如 @NotNull@Size@Pattern 等,以指定属性的验证规则。

示例使用 @Valid 的实体类:

public class SysUploadDto {

    @Schema(description = "文件名")
    private String fileName;

    @Schema(description = "文字总大小")
    private Integer totalSize;

    @Schema(description = "文件后缀")
    private String fileSuffix;

    @Schema(description = "文件的唯一标识")
    @NotNull(message = "文件不能为null")
    private String identifier;

    @Schema(description = "文件返回的路径")
    private String filePath;

    @Schema(description = "当前分片的序号")
    @Max(1)
    private Integer chunkNumber;

    @Schema(description = "当前分片的大小")
    private Float chunkSize;

    @Schema(description = "分片的实际大小")
    private Float currentChunkSize;

    @Schema(description = "总分片数")
    @NotNull(message = "文件不能为null")
    private Integer totalChunks;

    @Schema(description = "文件大小"  )
    @NotNull(message = "文件不能为null")
    private MultipartFile file;
}
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

在使用 @Valid 注解时,需要在控制器的请求处理方法上添加 @Validated 注解,以触发验证过程。

示例使用 @Valid@Validated 结合的方法签名:

@PostMapping("/upload")
public BoResult upload(@Valid @RequestBody SysUploadDto param) {
    // TODO ----
}
1
2
3
4

总结:

  • @Validated 是 Spring 框架中的验证注解,用于对方法参数进行验证。
  • @Valid 是 Java 标准库中的验证注解,用于对对象的属性进行验证。
  • @Validated@Valid 可以结合使用,以对方法参数和属性进行验证。

# 具体实现效果

image-20230527200025755

Last Updated: 10/30/2024, 10:57:00 AM