SpringBoot 中的参数校验:构建健壮应用的基石

前言

在开发Web应用时,处理用户输入是不可避免的一环。然而,用户输入往往充满不确定性,可能是格式不正确、类型不匹配,甚至包含恶意内容。为了确保应用的稳定性和安全性,对输入参数进行有效校验显得尤为重要。Spring Boot,作为当前最流行的Java开发框架之一,通过其丰富的特性和集成的库,为我们提供了一套高效、灵活的参数校验机制。本文将深入探讨Spring Boot中的参数校验技术,包括基于JSR-303/JSR-349(Bean Validation)的注解校验、自定义校验器以及如何在不同场景下应用这些校验技术,从而帮助你构建更加健壮、易于维护的Spring Boot应用。
一、为什么需要参数校验
在Web应用中,用户输入是数据流动的起点。然而,用户输入的数据往往难以预测和控制,可能包含各种不符合预期的情况。如果不对这些输入进行校验,就可能导致应用出现各种异常,如类型转换错误、数据格式错误、业务逻辑错误等。这些错误不仅会影响用户体验,还可能对应用的安全性和稳定性构成威胁。因此,在数据进入应用的核心处理流程之前,进行严格的参数校验是非常必要的。
二、常用的校验注解
Spring Boot支持的校验注解非常丰富,包括但不限于:

  • @NotNull:确保字段或参数的值不为null。
  • @NotEmpty:确保字符串、集合或数组不为null且不为空(对于字符串而言,长度大于0;对于集合或数组而言,元素个数大于0)。
  • @NotBlank:仅适用于字符串,确保字段或参数的值不为null且去除首尾空白字符后的长度大于0。
  • @Size(min=value, max=value):限制字符串、集合或数组的长度或元素个数。
  • @Email:确保字段或参数的值是一个有效的电子邮件地址。
  • @Pattern(regex=value):使用正则表达式校验字段或参数的值。

接下来,我们将详细讲解如何在Spring Boot项目中应用这些校验注解,以及如何处理校验失败的情况。同时,我们还将探讨如何自定义校验注解和校验器,以满足更加复杂的校验需求。

实体类参数校验

SpringBoot 使用校验注解不需要新引入任何依赖,是默认支持的。

首先,看在项目中是如何使用校验注解的。先来定义一个用户实体类:

import lombok.Builder;
import lombok.Data;
import javax.validation.constraints.*;
import java.util.List;

@Data
@Builder
public class UserEntity {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6到20个字符之间")
    private String password;

    @NotBlank(message = "邮箱不能为空")
    @Pattern(regexp = "^\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*(\\.\\w+)+$", message = "邮箱格式不正确")
    private String email;

    @NotEmpty(message = "至少包含一位好友")
    private List<UserEntity> friends;

}

编写一个注册接口用来测试:

import com.boot3.demo.commons.ResponseApi;
import com.boot3.demo.entity.UserEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;


/**
 * 用户信息处理器
 * ResponseApi 是我自定义的响应对象,为了方便测试也可以直接返回 UserEntity 对象
 * @CreateTime: 2024-07-01  14:50
 */
@RestController
@RequestMapping("users")
public class UserController {

    @PostMapping("register")
    public ResponseApi<UserEntity> register(@Valid @RequestBody UserEntity user) {
    
        return ResponseApi.success(user);
    }
}

准备就绪以后就可以请求 /users/register 接口验证一下对于 UserEntity 使用的注解是否生效。

{
    "username": "",
    "password": "12312321",
    "email": "123@qq.com",
    "friends": [
        {
            "username": "",
            "password": "1234567",
            "email": "123@qq.com",
            "friends": [
                {}
            ]
        }
    ]
}
  • 测试1:
    image.png
  • 测试2:
    image.png
  • 测试3:
    image.png
  • 测试4:
    image.png

这里你应该注意到了,接口的响应数据对于用户而言是非常的友好的,意思清晰明了,这个是因为我在项目中定义了全局异常处理器统一处理由参数校验所抛出的异常并进一步对响应结果进行封装的结果。

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.boot3.demo.commons.ResponseApi;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ValidationException;
import java.util.Optional;

/**
 * 全局异常处理
 *
 * @CreateTime: 2024-06-20  13:40
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    public final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 拦截实体类参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    public ResponseApi validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        logger.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return ResponseApi.error(exceptionStr);
    }

    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(Throwable.class)
    public ResponseApi defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        logger.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return ResponseApi.error();
    }

    private String getUrl(HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getQueryString())) {
            return request.getRequestURL().toString();
        }
        return request.getRequestURL().toString() + "?" + request.getQueryString();
    }
}

至此,对于实体类的参数校验已经完成,接下来再讲一讲如果不是实体类传参,而是单个或多个String、Integer 等类型的传参应该怎么处理。

普通参数校验

假如我现在再定义一个根据用户名称获取用户信息的接口,如下:

/**
 * 用户信息处理器
 * @CreateTime: 2024-07-01  14:50
 */
@RestController
@RequestMapping("users")
@Validated // 普通参数校验需要和这个注解一起使用,不然不会生效。
public class UserController {

    @PostMapping("register")
    public ResponseApi<UserEntity> register(@Valid @RequestBody UserEntity user) {

        return ResponseApi.success(user);
    }

    @GetMapping("getUserByName")
    public ResponseApi<UserEntity> getUserByName(@RequestParam("username") @NotBlank String username) {

        return ResponseApi.success(test(username));
    }

    /**
     * 为了测试,这里就随便写一个模拟查询数据库操作
     * @CreateTime: 2024-07-01  14:50
     */
    public UserEntity test(String username) {

        return UserEntity
                .builder()
                .username("李白")
                .password("admin")
                .email("admin@qq.com")
                .build();
    }

}

同样的,准备就绪以后就可以请求 /users/getUserByName 接口验证一下对于 username 使用的注解是否生效。

  • 测试1:
    这一次,你可能会发现接口的响应结果和上面不一样,并不是很友好,打开控制台一看,嗯?抛了一个叫 ConstraintViolationException 的异常,是正常拦截成功了,但是好像并没有被全局异常控制器中的 validExceptionHandler() 方法处理,这是因为validExceptionHandler() 方法指定拦截 项目中抛出的 MethodArgumentNotValidException 异常,所以它不在拦截范围内。
    image.png
  • 测试2:
    既然知道它抛出的是什么异常,那好办,在异常处理器中再添加一个方法拦截这个异常就好了,如下:
/**
 * 拦截普通参数验证异常
 */
@ExceptionHandler(value = {ConstraintViolationException.class})
public ResponseApi validExceptionHandler(HttpServletRequest request, ConstraintViolationException  ex) {
    logger.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), ex.getMessage());
    return ResponseApi.error(ex.getMessage());
}

再次请求一下:
image.png
因为没有对 @NotBlank 注解指定话术,所以展示的为默认话术,可以通过 @NotBlank(message = "用户名不能为空") 自定义拦截话术。
至此,普通参数的校验也完成了。

自定义校验规则

书接上回,假如现在有个需求要求用户的名称是唯一的,不能重复。如果按照以前的习惯写这个需求可能是这样的:

@PostMapping("register")
public ResponseApi<UserEntity> register(@Valid @RequestBody UserEntity user) {
    // 前面校验都通过了,获取出用户的名称
    String username = user.getUsername();
    // 根据用户名查询数据库
    UserEntity userInfo = getUserByName(username);
    // 判断如果 userInfo 不为空就代表用户名重复了
    if (userInfo != null) {
        return ResponseApi.error("用户名已存在");
    }
    return ResponseApi.success(user);
}

至此,这个需求就完成了,可是我不想这样写,我就想通过加一个注解就能解决用户名重复的问题,那怎么办呢?
好办,按照下面步骤来,先定一个注解:

想要了解以下元注解的含义请移步附录1

/**
 * 自定义唯一值参数校验注解
 */
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueValidator.class) // 指定实现校验逻辑的类
public @interface UniqueValue {

    String message() default "用户名不能重复";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

创建一个校验器:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 自定义唯一参数校验器
 */
public class UniqueValidator implements ConstraintValidator<UniqueValue, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // value 就是入参,比如在用户名上面加了注解,那这个值就是用户名,这里根据用户名查询一下数据库
        return getUserByName(value);
    }

    /**
     * 模拟查询数据库,如果是张三就代表重复了
     */
    private boolean getUserByName(String userName) {
        if ("张三".equals(userName)) {
            return false;
        }
        return true;
    }
}

接下来在 UserEntity 对象的用户名上加上 @UniqueValidator 注解:

@Data
@Builder
public class UserEntity {

    @NotBlank(message = "用户名不能为空")
    @UniqueValue(message = "用户名已存在")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在6到20个字符之间")
    private String password;

    @NotBlank(message = "邮箱不能为空")
    @Pattern(regexp = "^\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*(\\.\\w+)+$", message = "邮箱格式不正确")
    private String email;

    @NotEmpty(message = "至少包含一位好友")
    private List<UserEntity> friends;

}

然后请求再次请求 /users/register 接口:
image.png
至此,自定义校验规则也完成了,整个流程都非常简单,赶快打开电脑练习一下吧!

附录1

在这个自定义注解@UniqueValue的定义中,使用了几个Java注解(也称为元注解)来定义其特性和行为。这些元注解分别是@Documented、@Target、@Retention和@Constraint。下面我将依次详细解释这些元注解的含义:

  1. @Documented:
    • @Documented注解表明该自定义注解(@UniqueValue)在通过javadoc等工具生成文档时,应该被包含进去。这意味着当你查看使用@UniqueValue注解的类的文档时,你可以看到这个注解的信息。默认情况下,自定义注解不会在javadoc中显示。
  2. @Target:
    • @Target注解用于指定被注解的注解(@UniqueValue)可以应用的Java元素类型。在这个例子中,@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })表明@UniqueValue可以应用于方法、字段、注解类型、构造器以及方法参数上。这为@UniqueValue的使用提供了灵活性,可以根据需要将其应用于不同类型的元素上。
  3. @Retention:
    • @Retention注解指定了被注解的注解(@UniqueValue)的保留策略。保留策略决定了注解在什么级别上可用:源代码(SOURCE)、类文件(CLASS)或运行时(RUNTIME)。在这个例子中,@Retention(RUNTIME)表明@UniqueValue注解在运行时是可以通过反射被读取的。这对于在运行时进行注解处理(如参数校验)是必需的。
  4. @Constraint(validatedBy = UniqueValidator.class):
    • @Constraint是Bean Validation API(JSR 303/349)中定义的,用于标识一个自定义的校验注解。它不是Java标准库中的一部分,而是Bean Validation规范的一部分。
    • validatedBy = UniqueValidator.class指定了实现该注解校验逻辑的类。在这个例子中,UniqueValidator类将包含校验@UniqueValue注解所标记的元素是否满足“唯一性”逻辑的代码。这意味着当Spring Boot或任何支持Bean Validation的框架遇到@UniqueValue注解时,它会调用UniqueValidator类来执行实际的校验逻辑。

总结来说,这些元注解定义了@UniqueValue注解的基本属性和行为,包括它是否应该被文档化、它可以应用于哪些Java元素、它在何处被保留以及它的校验逻辑由哪个类实现。这些定义对于创建功能丰富且易于使用的自定义校验注解至关重要。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/778089.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python中解决os.listdir命令读取文件乱序问题方法

Python中使用对话框批量打开文件时出现乱序问题的解决方法 一、问题描述二、os.listdir读取文件乱序问题解决方法 欢迎学习交流&#xff01; 邮箱&#xff1a; z…1…6.com 网站&#xff1a; https://zephyrhours.github.io/ 一、问题描述 有时候为了方便&#xff0c;我们在进…

MySQL之备份与恢复(五)

备份与恢复 备份数据 符号分隔文件备份 可以使用SQL命令SELECT INTO OUTFILE以符号分隔文件格式创建数据的逻辑备份。(可以用mysqldump的 --tab选项导出到符号分隔文件中)。符号分隔文件包含以ASCII展示的原始数据&#xff0c;没有SQL、注释和列名。下面是一个导出为逗号分隔…

vb.netcad二开自学笔记3:启动与销毁

Imports Autodesk.AutoCAD.ApplicationServicesImports Autodesk.AutoCAD.EditorInputImports Autodesk.AutoCAD.RuntimePublic Class WellcomCADImplements IExtensionApplicationPublic Sub Initialize() Implements IExtensionApplication.InitializeMsgBox("net程序已…

ePTFE膜(膨体聚四氟乙烯膜)应用前景广阔 本土企业技术水平不断提升

ePTFE膜&#xff08;膨体聚四氟乙烯膜&#xff09;应用前景广阔 本土企业技术水平不断提升 ePTFE膜全称为膨体聚四氟乙烯膜&#xff0c;指以膨体聚四氟乙烯&#xff08;ePTFE&#xff09;为原材料制成的薄膜。ePTFE膜具有耐化学腐蚀、防水透气性好、耐候性佳、耐磨、抗撕裂等优…

【深度学习】-WASB-调试说明

要改这么几个地方&#xff1a; 代码仓库&#xff1a;/Desktop/code/python_project/WASB-SBDT-main/ 篮球数据集xx_xx_11.xml只保留最后一个11.xml 并把11下直接放置11 video&#xff1a; 这里的东西被我改了&#xff0c;要以仓库为准

git pull拉取显示Already up-to-date,但文件并没有更新

1、问题&#xff1a; 使用git pull拉取远程仓库代码&#xff0c;显示更新成功&#xff08;Already up-to-date&#xff09;&#xff0c;但是本地代码没有更新 这是因为本地有尚未提交的更改&#xff0c;和远程代码有冲突导致无法更新 2、解决方法&#xff1a; 可以使用git s…

Fastjson首字母大小写问题

1、问题 使用Fastjson转json之后发现首字母小写。实体类如下&#xff1a; Data public class DataIdentity {private String BYDBSM;private String SNWRSSJSJ;private Integer CJFS 20; } 测试代码如下&#xff1a; public static void main(String[] args) {DataIdentit…

多个tomcat同时使用 不设置CATALINA_HOME环境变量

通常一台服务器只使用一个tomcat&#xff0c;设置一个CATALINA_HOME的环境变量。但有些时候需要一台服务器启动多个tomcat&#xff0c;那就不能设置CATALINA_HOME了&#xff01;因为会串~ 我们可以在对应tomcat的startup.bat启动脚本中&#xff0c;加入对应的CATALINA_HOME。 …

Raylib 坐标系

draftx 符号调整为正数 发现采样坐标系原点0&#xff0c;0 在左上角&#xff0c;正方向 右&#xff0c;下 绘制坐标系 原点0&#xff0c;0 在左下角&#xff0c;正方向 右&#xff0c;上 拖拽可得 #include <raylib.h> // 重整原因&#xff1a;解决新函数放大缩小之下…

Appium+python自动化(四十一)-Appium自动化测试框架综合实践 - 即将落下帷幕(超详解)

1.简介 今天我们紧接着上一篇继续分享Appium自动化测试框架综合实践 - 代码实现。到今天为止&#xff0c;大功即将告成&#xff1b;框架所需要的代码实现都基本完成。 2.data数据封装 2.1使用背景 在实际项目过程中&#xff0c;我们的数据可能是存储在一个数据文件中&#x…

智慧交通运行监测与应急指挥中心方案

建设目标 建立感知层数据的实时采集以及数据处理&#xff0c;实现监测预警自动化和智能化&#xff1b;推动交通运输数据资源开放共享&#xff0c;打破数据资源壁垒&#xff0c;与城市各部门数据建立共享交换机制&#xff0c;实现应急指挥的协同化&#xff1b;充分运用大数据、互…

新产品或敏捷项目过程 SOP,附带流程图及流程规范

一、项目启动 项目背景和目标明确 市场调研结果分析&#xff0c;确定新产品的需求和市场机会。制定明确的项目目标&#xff0c;包括产品特性、上市时间、预期收益等。 组建项目团队 确定项目经理、产品经理、开发人员、测试人员、市场人员等角色。明确各成员的职责和权限。 项目…

Apache Seata应用侧启动过程剖析——注册中心与配置中心模块

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata应用侧启动过程剖析——注册中心与配置中心模块 前言 在Seata的应用侧&#xf…

Docker逃逸CVE-2019-5736、procfs云安全漏洞复现,全文5k字,超详细解析!

Docker容器挂载procfs 逃逸 procfs是展示系统进程状态的虚拟文件系统&#xff0c;包含敏感信息。直接将其挂载到不受控的容器内&#xff0c;特别是容器默认拥有root权限且未启用用户隔离时&#xff0c;将极大地增加安全风险。因此&#xff0c;需谨慎处理&#xff0c;确保容器环…

迅捷PDF编辑器合并PDF

迅捷PDF编辑器是一款专业的PDF编辑软件&#xff0c;不仅支持任意添加文本&#xff0c;而且可以任意编辑PDF原有内容&#xff0c;软件上方的工具栏中还有丰富的PDF标注、编辑功能&#xff0c;包括高亮、删除线、下划线这些基础的&#xff0c;还有规则或不规则框选、箭头、便利贴…

VRPTW(MATLAB):常春藤算法(IVY)求解带时间窗的车辆路径问题VRPTW,MATLAB代码

详细介绍 VRPTW&#xff08;MATLAB&#xff09;&#xff1a;常春藤算法&#xff08;Ivy algorithm&#xff0c;IVY&#xff09;求解带时间窗的车辆路径问题VRPTW&#xff08;提供MATLAB代码&#xff09;-CSDN博客 ********************************求解结果******************…

SpringBoot 生产实践:没有父 starter 的打包问题

文章目录 前言一、搜索引擎二、Chat GPT三、官方文档四、小结推荐阅读 前言 今天刚准备写点文章&#xff0c;需要 SpringBoot 项目来演示效果。一时心血来潮&#xff0c;没有采用传统的方式&#xff08;即通过引入 spring-boot-starter-parent 父工程的方式&#xff09;。 &l…

昇思25天学习打卡营第15天|linchenfengxue

Pix2Pix实现图像转换 Pix2Pix概述 Pix2Pix是基于条件生成对抗网络&#xff08;cGAN, Condition Generative Adversarial Networks &#xff09;实现的一种深度学习图像转换模型&#xff0c;该模型是由Phillip Isola等作者在2017年CVPR上提出的&#xff0c;可以实现语义/标签到…

16-JS封装:extend方法

目录 一、封装需求 二、实现1&#xff1a;jQuery.extend 三、实现2&#xff1a;通过原型jQuery.fn.extend 四、优化 一、封装需求 封装需求&#xff1a; $.extend&#xff1a; var obj{ name:"xxx",age:18} var obj3{ gender:"女"} var obj2{}; 将obj、…

如何注册微信公众号

如何注册微信公众号 如何注册一个微信公众号 &#x1f60a;&#x1f4f1;摘要引言正文内容1. 准备工作内容定位和受众群体公众号名称和头像 2. 网页注册流程第一步&#xff1a;访问微信公众平台第二步&#xff1a;选择账户注册类型第三步&#xff1a;填写基本信息第四步&#x…