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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: springboot全局异常实现以及@Valid和@Validated优雅实现入参验证

前序

为什么要有全局异常❓

  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;
    }
}

全局异常捕获配置类

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("")));
    }
}

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

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

效果展示

系统异常捕获

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

自定义异常捕获

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

@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) {
    // 处理创建用户的逻辑
}

@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;
}

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

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

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

总结:

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

具体实现效果

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
JSON 安全 算法
|
3月前
|
前端开发 Java 数据安全/隐私保护
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
文章通过一个简单的SpringBoot项目,详细介绍了前后端如何实现用户登录功能,包括前端登录页面的创建、后端登录逻辑的处理、使用session验证用户身份以及获取已登录用户信息的方法。
493 2
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
|
2月前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
60 1
|
3月前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
3月前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
382 10
|
3月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
237 6
|
5月前
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
2223 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
|
5月前
|
SQL Java 数据库连接
springboot+mybatis+shiro项目中使用shiro实现登录用户的权限验证。权限表、角色表、用户表。从不同的表中收集用户的权限、
这篇文章介绍了在Spring Boot + MyBatis + Shiro项目中,如何使用Shiro框架实现登录用户的权限验证,包括用户、角色和权限表的设计,以及通过多个表查询来收集和验证用户权限的方法和代码实现。
springboot+mybatis+shiro项目中使用shiro实现登录用户的权限验证。权限表、角色表、用户表。从不同的表中收集用户的权限、
|
5月前
|
消息中间件 Java 开发工具
【Azure 事件中心】Spring Cloud Stream Event Hubs Binder 发送Event Hub消息遇见 Spec. Rule 1.3 - onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled serially 异常
【Azure 事件中心】Spring Cloud Stream Event Hubs Binder 发送Event Hub消息遇见 Spec. Rule 1.3 - onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled serially 异常
|
5月前
|
Java Spring
【Azure 事件中心】Spring Boot 集成 Event Hub(azure-spring-cloud-stream-binder-eventhubs)指定Partition Key有异常消息
【Azure 事件中心】Spring Boot 集成 Event Hub(azure-spring-cloud-stream-binder-eventhubs)指定Partition Key有异常消息