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> {@Overridepublic 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.mzph.cn/news/864849.shtml

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

相关文章

【触想智能】工业平板电脑在新能源领域上的应用分析

工业平板电脑是一种具有高性能和稳定性的计算机设备&#xff0c;适用于在恶劣环境下进行数据采集、运营管理和现场操作。 随着新能源技术的快速发展&#xff0c;工业平板电脑不断地得到应用&#xff0c;并且已成为新能源领域中的重要工具之一。本文将从四个方面探讨工业平板电脑…

WSO2 products 文件上传漏洞(CVE-2022-29464)

前言 CVE-2022-29464 是一个影响多个 WSO2 产品的严重远程代码执行&#xff08;RCE&#xff09;漏洞。这些产品包括 WSO2 API Manager、WSO2 Identity Server 和 WSO2 Enterprise Integrator 等。由于用户输入验证不当&#xff0c;该漏洞允许未经身份验证的攻击者在服务器上上…

代码随想录算法训练营第九天|151.翻转字符串里的单词、右旋字符串、28. 实现 strStr()、459.重复的子字符串

打卡Day9 1.151.翻转字符串里的单词2.右旋字符串3.28. 实现 strStr()4.459.重复的子字符串 1.151.翻转字符串里的单词 题目链接&#xff1a;翻转字符串里的单词 文档讲解&#xff1a; 代码随想录 思路&#xff1a;首先&#xff0c;移除多余的空格&#xff1b;然后&#xff0c…

TensorRT学习(二)TensorRT使用教程(Python版)

本文适合快速了解TensorRT使用的整体流程,具体细节还是建议参考TensorRT的官方文档。 加速原理: 加速原理比较复杂,它将会根据显卡来优化算子,以起到加速作用(如下图所示)。简单的来说,就是类似于你出一个公式1+1+1,而你的显卡支持乘法,直接给你把这个公式优化成了1*…

生成式AI赋能金融信贷:减少信用评分偏差

信用评分在确定谁获得信贷以及以何种条件获得信贷方面发挥着关键作用。然而&#xff0c;尽管这一点很重要&#xff0c;但传统的信用评分系统长期以来一直受到一系列关键问题的困扰——从偏见和歧视&#xff0c;到有限的数据考虑和可扩展性挑战。例如&#xff0c;一项针对美国贷…

【代码随想录】【算法训练营】【第56天】 [卡码98]所有可达路径

前言 思路及算法思维&#xff0c;指路 代码随想录。 题目来自 卡码网。 day 56&#xff0c;周二&#xff0c;继续ding~ 题目详情 [卡码98] 所有可达路径 题目描述 卡码98 所有可达路径 解题思路 前提&#xff1a; 思路&#xff1a; 重点&#xff1a; 代码实现 C语言…

【JPCS出版,PSESG 2024,8月16-18】2024年电力系统工程与智能电网国际学术会议

2024年电力系统工程与智能电网国际学术会议(PSESG 2024)于2024年8月16-18日在中国北京隆重召开。 会议旨在为从事“电力系统工程”、“智能电网”、“储能技术”等领域的专家学者、工程技术人员、研发人员提供一个共享科研成果和前沿技术&#xff0c;了解学术发展趋势&#xf…

GPT-5即将登场:AI赋能下的未来工作与日常生活新图景

随着OpenAI首席技术官米拉穆拉蒂在近期采访中的明确表态&#xff0c;GPT-5的发布已不再是遥不可及的梦想&#xff0c;而是即将在一年半后与我们见面的现实。这一消息无疑在科技界乃至全社会引发了广泛关注和热烈讨论。从GPT-4到GPT-5的飞跃&#xff0c;被形容为从高中生到博士生…

SpringBoot之内容协商

现象演示 假设有一个需求是根据终端的不同&#xff0c;返回不同形式的数据&#xff0c;比如 PC 端需要以 HTML 格式返回数据&#xff0c;APP、小程序端需要以 JSON 格式返回数据。这时我们是 coding 几个相似的接口&#xff1f;还是在一个接口里面做复杂判断处理&#xff1f;两…

SQL执行慢排查以及优化思路

数据库服务器的优化步骤 当我们遇到数据库调优问题的时候&#xff0c;该如何思考呢&#xff1f;我把思考的流程整理成了下面这张图。 整个流程划分成了观察&#xff08;Show status&#xff09;和行动&#xff08;Action&#xff09;两个部分。字母 S 的部分代表观察&#xf…

LSH算法:高效相似性搜索的原理与Python实现I

局部敏感哈希&#xff08;LSH&#xff09;技术是快速近似最近邻&#xff08;ANN&#xff09;搜索中的一个关键方法&#xff0c;广泛应用于实现高效且准确的相似性搜索。这项技术对于许多全球知名的大型科技公司来说是不可或缺的&#xff0c;包括谷歌、Netflix、亚马逊、Spotify…

分文件编译(简单学生系统)

定义学生基本信息 ①输出所有学生信息 ②删除某个学生后&#xff0c;输出所有学生信息 ③修改某个学生信息后&#xff0c;输出所有学生信息 ④查找某个学生的信息 main.c #include"k11*.h" int main(int argc, const char *argv[]) {struct student p[4]{{"…

Echarts-柱状图

1.案例1 1.1代码 option = {textStyle: {color: #fff // 标题文字颜色为白色},tooltip: {trigger: axis,axisPointer: {type: shadow,},},legend: {textStyle: {color: white}},grid: {top: 15%,left: 4%,right: 4%,bottom: 7%,containLabel: true},xAxis:{type: category,da…

仿全民飞机大战射击网页游戏源码

仿全民飞机大战设计网页游戏源码&#xff0c;画质精美的飞机大战手机端游戏源码 微信扫一扫免费下载源码

Mac|install vue

安装Node&#xff1a;Node.js — Download Node.js 选择系统为mac&#xff0c;安装步骤在终端输入 &#xff08;放文字版在这里&#xff5e;方便复制&#xff09; # installs nvm (Node Version Manager) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/ins…

78.Vue 3 重用性模态框组件

模态框是大多数 Web 应用程序中的基本构建块。虽然最初实现起来可能看起来有点棘手&#xff0c;但实际上&#xff0c;使用 Vue 和一些 Flexbox 技巧&#xff0c;这不仅可行&#xff0c;而且非常简单。 让我们一起实现一个基础的模态框组件。 架构如下&#xff1a; AppModal.vue…

docker-compose搭建prometheus、grafana

一、安装prometheus 1、安装 version: 3.1services:prometheus:image: prom/prometheus:v2.48.0container_name: prometheushostname: prometheusrestart: alwaysvolumes:- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml- ./prometheus/:/etc/prometheus/port…

第二篇——始计篇:“计”是最早的SWOT分析

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么&#xff1f; 四、总结五、升华 一、背景介绍 第二次详读孙子兵法&#xff0c;当初听讲解的时候&#xff0c;就觉得自己…

WPF UI 3D 多轴 机械臂 stl 模型UI交互

鼠标交互&#xff08;没有强调场景的变换&#xff09; 鼠标命中测试&#xff08;HitTest 不推荐&#xff09; 平面对象加载 数据绑定&#xff08;数据与动作&#xff09; 环境配置与相关方法 模型准备&#xff1a;Blender/SolidWorks 模型导入 HelixToolkit更多案例…

驱动LSM6DS3TR-C实现高效运动检测与数据采集(6)----FIFO数据读取与配置

驱动LSM6DS3TR-C实现高效运动检测与数据采集.6--FIFO数据读取与配置 概述视频教学样品申请源码下载主要内容生成STM32CUBEMX串口配置IIC配置CS和SA0设置ICASHE修改堆栈串口重定向参考驱动程序FIFO参考程序初始化管脚获取ID复位操作设置量程BDU设置设置速率FIFO读取程序设置FIFO…