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

news2024/10/6 9:26:14

前言

在开发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.coloradmin.cn/o/1886138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue3 特点以及优势-源码解剖

Vue3 特点以及优势-Vue3.4源码解剖 Vue3 特点以及优势 1.声明式框架 命令式和声明式区别 早在 JQ 的时代编写的代码都是命令式的&#xff0c;命令式框架重要特点就是关注过程声明式框架更加关注结果。命令式的代码封装到了 Vuejs 中&#xff0c;过程靠 vuejs 来实现 声明式代…

微软与OpenAI/谷歌与三星的AI交易受欧盟重点关注

近日&#xff0c;欧盟委员会主管竞争事务的副主席玛格丽特维斯塔格(Margrethe Vestager)在一次演讲中透露&#xff0c;欧盟反垄断监管机构将就微软与OpenAI的合作&#xff0c;以及谷歌与三星达成的AI协议寻求更多第三方意见。这意味着微软与 OpenAI、谷歌与三星的 AI 交易及合作…

数据库操作-DML和DQL

DML DML英文全称是Data Manipulation Language(数据操作语言)&#xff0c;用来对数据库中表的数据记录进行增、删、改操作。 添加数据&#xff08;INSERT&#xff09; 1.指定字段添加数据&#xff1a; insert into 表名 ( 字段名 1, 字段名 2) values ( 值 1, 值 2); 2…

守望先锋2卡顿/丢包?守望先锋2延迟高怎么降低

守望先锋2/ow2新赛季已经上线&#xff0c;想必很多玩家都已经进入游戏体验过&#xff0c;新的主题英雄和Push地图和改版后的斗兽场玻璃墙。不过虽然新赛季内容满满&#xff0c;有趣又好玩&#xff0c;但是架不住服务器拉跨&#xff0c;近期也有不少玩家吐槽新赛季问题频发&…

短信接口平台的核心功能有哪些?如何使用?

短信接口平台怎么有效集成&#xff1f;选择短信接口平台的技巧&#xff1f; 短信接口平台作为一种重要的通信工具&#xff0c;广泛应用于各种企业和组织。通过短信接口平台&#xff0c;企业能够高效、便捷地与客户进行互动和沟通。AoKSend将详细介绍短信接口平台的核心功能。 …

【博主推荐】HTML5实现简洁好看的个人简历网页模板源码

文章目录 1.设计来源1.1 主界面1.2 关于我界面1.3 工作经验界面1.4 学习教育界面1.5 个人技能界面1.6 专业特长界面1.7 朋友评价界面1.8 获奖情况界面1.9 联系我界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c…

大模型技术在辅助学习中的应用

大模型技术在辅助学习中的应用场景非常广泛&#xff0c;以下是一些典型示例。大模型技术在辅助学习中具有广阔的应用前景&#xff0c;可以为学生提供更加个性化、智能化和高效的学习体验。随着大模型技术的不断发展&#xff0c;我们可以期待在未来看到更多创新应用。北京木奇移…

bmob Harmony鸿蒙快速开发搜索功能

搜索功能是很多应用都需要的功能。在很多平台上&#xff0c;要开发一个兼容性较好的搜索功能都还是需要添加比较多的视图代码的。 为了解决这个问题&#xff0c;鸿蒙ArkUI提供了一个快速添加搜索功能的视图组件给我们&#xff0c;结合Bmob Harmony鸿蒙SDK的搜索能力&#xff0…

Spark2.0

目录 10.3 Spark运行架构 10.3.1 基本概念 10.3.2 架构设计 ​编辑 10.3.3 Spark运行基本流程 Spark运行架构特点 10.3 Spark运行架构 10.3.1 基本概念 RDD &#xff1a;是 Resillient Distributed Dataset &#xff08;弹性分布式数据集&#xff09;的简称&#xff0c;是分…

前端开发中的常见问题及解决方法

前端开发是一个充满挑战和乐趣的领域。然而&#xff0c;在开发过程中&#xff0c;开发者常常会遇到各种各样的问题。本文将介绍一些前端开发中常用或者经常遇到的问题&#xff0c;并提供相应的解决方法&#xff0c;帮助你提高开发效率和解决问题的能力。 一. 页面布局问题 问题…

git 禁止dev合并到任何其他分支

创建 pre-merge-commit 钩子 导航到 Git 仓库的钩子目录&#xff1a; cd /path/to/your/repo/.git/hooks创建或编辑 pre-merge-commit 钩子&#xff1a; 也可以通过指令创建 nano pre-merge-commit在钩子文件中添加以下代码&#xff1a; #!/bin/sh# 获取当前分支名称 curr…

Audition 2024 for Mac/Win:音频处理的极致艺术

Adobe Audition 2024是一款面向Mac和Windows用户的顶级音频录制和编辑软件&#xff0c;以其卓越的性能和丰富的功能&#xff0c;满足了专业音乐制作、音频后期处理、播客录制等多个领域的需求。 Audition 2024提供了全面的音频处理功能&#xff0c;包括高效的录音、混音、编辑…

精密仪器中微型丝杆延长使用寿命的技巧!

微型丝杆是现代小型化机械中常用的传动元件&#xff0c;其具有高精度、高刚性、高效率等特点。被广泛应用在各种精密仪器当中&#xff0c;如&#xff1a;激光打印机、光学仪器、显微镜、高精度相机、医疗器械、智能家居、机器人等设备&#xff0c;可见在制造业中有无可替代的作…

CredSSP 远程执行代码漏洞(CVE-2018-0886)漏洞处理过程

Microsoft Windows CredSSP 远程执行代码漏洞(CVE-2018-0886)【原理扫描】 此漏洞被定级为高危漏洞&#xff0c;因此需要修复处理&#xff01; 【处理建议一】 凭据分配修改Oracle修正 运行 --> gpedit.msc --> 本地组策略编辑器 计算机配置 --> 管理模板 --> …

动手RAG: ocr调研

对于rag应用来说&#xff0c;文档是第一步&#xff0c;对于部分扫描件的文件来讲&#xff0c;主要就需要OCR. OCR tesseractppocrmmocr OCR包含几类&#xff0c; 自然场景中的文字识别&#xff0c;文档中的文字识别pipeline: 文本检测&#xff0c;文本识别&#xff0c;文…

巴比达内网穿透:深度剖析其在解决远程连接挑战中的技术优势

在信息技术日新月异的今天&#xff0c;远程协作与管理的需求日益增长&#xff0c;但内网环境的隔离性一直是横亘在高效远程操作面前的一道坎。本文将深入探讨一款专为打破此壁垒而生的工具——巴比达内网穿透&#xff0c;如何以其技术创新和高效性能&#xff0c;成为解决远程连…

每日一题——Python实现蓝桥杯 单词分析(举一反三+思想解读+逐步优化)五千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的写法 代码分析 时间复杂度分析 空间复杂度分析 总结 我要更强 方法一&#x…

左值右值, 左值引用右值引用,完美转发

一. 左值和右值 左值: 可以取地址的对象 右值: 不可以取地址的对象 double x1.0, y 2.0; 1; // 字面量, 不可取地址, 是右值 x y; // 表达式返回值, 不可取地址, 是右值 max(x, y); // 传值返回函数的返回值 (非引用返回)总结就是: 根据是否可以取地址来区分是左值还…

Pandas实战秘籍:轻松驾驭重复值与异常值的处理艺术,让数据清洗更高效!

1.导包 import numpy as np import pandas as pd2.删除重复行 def make_df(indexs,columns):data [[str(j)str(i) for j in columns] for i in indexs]df pd.DataFrame(datadata,indexindexs,columnscolumns)return df使用 duplicated() 函数检测重复的行 返回元素为布尔类…

昇思MindSpore学习总结二——张量

1、张量 张量tensor表示的是一个多维的矩阵&#xff0c;比如零维就是一个点&#xff0c;一维就是向量&#xff0c;二维就是一般的矩阵&#xff0c;多维就相当于一个多维的数组&#xff0c;这和numpy是对应的&#xff0c;而且PyTorch的Tensor和numpy的ndarray可以相互转换&#…