【Spring】SpringBoot的10个参数验证技巧

news2024/12/23 14:33:50

这里写目录标题

  • 前言
  • 1.使用验证注解
  • 2 使用自定义验证注解
  • 3 在服务器端验证
  • 4 提供有意义的错误信息
  • 5 将 i18n 用于错误消息
  • messages.properties
  • 6 使用分组验证
  • 7 对复杂逻辑使用跨域验证
  • 8 对验证错误使用异常处理
  • 9 测试你的验证逻辑
  • 10 考虑客户端验证
  • 总结

请添加图片描述

前言

参数验证很重要,是平时开发环节中不可少的一部分,但是我想很多后端同事会偷懒,干脆不错,这样很可能给系统的稳定性和安全性带来严重的危害。那么在Spring Boot应用中如何做好参数校验工作呢,本文提供了10个小技巧,你知道几个呢?

1.使用验证注解

Spring Boot提供了内置的验证注解,可以帮助简单、快速地对输入字段进行验证,例如检查 null 或空字段、强制执行长度限制、使用正则表达式验证模式以及验证电子邮件地址。

一些最常用的验证注释包括:

@NotNull:指定字段不能为空。

@NotEmpty:指定列表字段不能为空。

@NotBlank:指定字符串字段不得为空或仅包含空格。

@Min 和 @Max:指定数字字段的最小值和最大值。

@Pattern:指定字符串字段必须匹配的正则表达式模式。

@Email:指定字符串字段必须是有效的电子邮件地址。

    具体用法参考下面例子:

public class User {
@NotNull
private Long id;

@NotBlank
@Size(min = 2, max = 50)
private String firstName;

@NotBlank
@Size(min = 2, max = 50)
private String lastName;

@Email
private String email;

@NotNull
@Min(18)
@Max(99)
private Integer age;

@NotEmpty
private List<String> hobbies;

@Pattern(regexp = "[A-Z]{2}\d{4}")
private String employeeId;

2 使用自定义验证注解

虽然 Spring Boot 的内置验证注释很有用,但它们可能无法涵盖所有情况。如果有特殊参数验证的场景,可以使用 Spring 的 JSR 303 验证框架创建自定义验证注释。自定义注解可以让你的的验证逻辑更具可重用性和可维护性。

假设我们有一个应用程序,用户可以在其中创建帖子。每个帖子都应该有一个标题和一个正文,并且标题在所有帖子中应该是唯一的。虽然 Spring Boot 提供了用于检查字段是否为空的内置验证注释,但它没有提供用于检查唯一性的内置验证注释。在这种情况下,我们可以创建一个自定义验证注解来处理这种情况。

首先,我们创建自定义约束注解UniqueTitle :

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueTitleValidator.class)
public @interface UniqueTitle {
String message() default “Title must be unique”;

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

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

}
接下来,我们创建一个PostRepository接口,目的是从数据库中检索帖子:

public interface PostRepository extends JpaRepository<Post, Long> {
Post findByTitle(String title);
}
然后我们需要定义验证器类 UniqueTitleValidator,如下所示:

@Component
public class UniqueTitleValidator implements ConstraintValidator<UniqueTitle, String> {

@Autowired
private PostRepository postRepository;

@Override
public boolean isValid(String title, ConstraintValidatorContext context) {
    if (title == null) {
        return true;
    }
    return Objects.isNull(postRepository.findByTitle(title));
}

}
UniqueTitleValidator实现了ConstraintValidator接口,它有两个泛型类型:第一个是自定义注解UniqueTitle,第二个是正在验证的字段类型(在本例中为String). 我们还自动装配了PostRepository 类以从数据库中检索帖子。

isValid()方法通过查询 PostRepository 来检查 title 是否为 null 或者它是否是唯一的。如果 title 为 null 或唯一,则验证成功,并返回 true。

定义了自定义验证注释和验证器类后,我们现在可以使用它来验证 Spring Boot 应用程序中的帖子标题:

public class Post {
@UniqueTitle
private String title;

@NotNull
private String body;

}
我们已将 @UniqueTitle 注释应用于 Post 类中的 title 变量。验证此字段时,这将触发 UniqueTitleValidator 类中定义的验证逻辑。

3 在服务器端验证

除了前端或者客户端做了验证意外,服务器端验证输入是至关重要的。它可以确保在处理或存储任何恶意或格式错误的数据之前将其捕获,这对于应用程序的安全性和稳定性至关重要。

假设我们有一个允许用户创建新帐户的 REST 端点。端点需要一个包含用户用户名和密码的 JSON 请求体。为确保输入有效,我们可以创建一个 DTO(数据传输对象)类并将验证注释应用于其字段:

public class UserDTO {

@NotBlank
private String username;

@NotBlank
private String password;

}
我们使用@NotBlank注解来确保username和password字段不为空或 null。

接下来,我们可以创建一个控制器方法来处理 HTTP POST 请求并在创建新用户之前验证输入:

@RestController
@RequestMapping(“/users”)
@Validated
public class UserController {
@Autowired
private UserService userService;

@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDto) {
    userService.createUser(userDto);
    return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
}

}
我们使用 Spring 的@Validated注解来启用方法级验证,我们还将 @Valid 注释应用于 userDto 参数以触发验证过程。

4 提供有意义的错误信息

当验证失败时,必须提供清晰简洁的错误消息来描述出了什么问题以及如何修复它。

这是一个示例,如果我们有一个允许用户创建新用户的 RESTful API。我们要确保姓名和电子邮件地址字段不为空,年龄在 18 到 99 岁之间,除了这些字段,如果用户尝试使用重复的“用户名”创建帐户,我们还会提供明确的错误消息或“电子邮件”。

为此,我们可以定义一个带有必要验证注释的模型类 User,如下所示:

public class User {

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

@NotBlank(message = "Email不能为空")
@Email(message = "无效的Emaild地址")
private String email;

@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄必须大于18")
@Max(value = 99, message = "年龄必须小于99")
private Integer age;

}
我们使用 message属性为每个验证注释提供了自定义错误消息。

接下来,在我们的 Spring 控制器中,我们可以处理表单提交并使用 @Valid 注释验证用户输入:

@RestController
@RequestMapping(“/users”)
public class UserController {
@Autowired
private UserService userService;

@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody User user, BindingResult result) {
    if (result.hasErrors()) {
        List<String> errorMessages = result.getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.toList());
        return ResponseEntity.badRequest().body(errorMessages.toString());
    }

    // save the user to the database using UserService
    userService.saveUser(user);

    return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
}

}

我们使用 @Valid 注释来触发 User 对象的验证,并使用 BindingResult 对象来捕获任何验证错误。

5 将 i18n 用于错误消息

如果你的应用程序支持多种语言,则必须使用国际化 (i18n) 以用户首选语言显示错误消息。

以下是在 Spring Boot 应用程序中使用 i18n 处理错误消息的示例

首先,在资源目录下创建一个包含默认错误消息的 messages.properties 文件

messages.properties

user.name.required=Name is required.
user.email.invalid=Invalid email format.
user.age.invalid=Age must be a number between 18 and 99.
接下来,为每种支持的语言创建一个 messages_xx.properties 文件,例如,中文的 messages_zh_CN.properties。

user.name.required=名称不能为空.
user.email.invalid=无效的email格式.
user.age.invalid=年龄必须在18到99岁之间.
然后,更新您的验证注释以使用本地化的错误消息

public class User {
@NotNull(message = “{user.id.required}”)
private Long id;

@NotBlank(message = "{user.name.required}")
private String name;

@Email(message = "{user.email.invalid}")
private String email;

@NotNull(message = "{user.age.required}")
@Min(value = 18, message = "{user.age.invalid}")
@Max(value = 99, message = "{user.age.invalid}")
private Integer age;

}

最后,在 Spring 配置文件中配置 MessageSource bean 以加载 i18n 消息文件

@Configuration
public class AppConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename(“messages”);
messageSource.setDefaultEncoding(“UTF-8”);
return messageSource;
}

@Bean
public LocalValidatorFactoryBean validator() {
    LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
    validatorFactoryBean.setValidationMessageSource(messageSource());
    return validatorFactoryBean;
}

}

现在,当发生验证错误时,错误消息将根据随请求发送的“Accept-Language”标头以用户的首选语言显示。

6 使用分组验证

验证组是 Spring Boot 验证框架的一个强大功能,允许您根据其他输入值或应用程序状态应用条件验证规则。

现在有一个包含三个字段的User类的情况下:firstName、lastName和email。我们要确保如果 email 字段为空,则 firstName 或 lastName 字段必须非空。否则,所有三个字段都应该正常验证。

为此,我们将定义两个验证组:EmailNotEmpty 和 Default。EmailNotEmpty 组将包含当 email 字段不为空时的验证规则,而 Default 组将包含所有三个字段的正常验证规则。

创建带有验证组的 User 类

public class User {
@NotBlank(groups = Default.class)
private String firstName;

@NotBlank(groups = Default.class)
private String lastName;

@Email(groups = EmailNotEmpty.class)
private String email;

// getters and setters omitted for brevity
public interface EmailNotEmpty {}
public interface Default {}

}
请注意,我们在User类中定义了两个接口,EmailNotEmpty和 Default。这些将作为我们的验证组。

接下来,我们更新Controller使用这些验证组

@RestController
@RequestMapping(“/users”)
@Validated
public class UserController {
public ResponseEntity createUser(
@Validated({org.example.model.ex6.User.EmailNotEmpty.class}) @RequestBody User userWithEmail,
@Validated({User.Default.class}) @RequestBody User userWithoutEmail)
{
// Create the user and return a success response

}

}
我们已将@Validated注释添加到我们的控制器,表明我们想要使用验证组。我们还更新了 createUser 方法,将两个 User 对象作为输入,一个在 email 字段不为空时使用,另一个在它为空时使用。

@Validated 注释用于指定将哪个验证组应用于每个 User 对象。对于 userWithEmail 参数,我们指定了 EmailNotEmpty 组,而对于 userWithoutEmail 参数,我们指定了 Default 组。

进行这些更改后,现在将根据“电子邮件”字段是否为空对“用户”类进行不同的验证。如果为空,则 firstName 或 lastName 字段必须非空。否则,所有三个字段都将正常验证。

7 对复杂逻辑使用跨域验证

如果需要验证跨多个字段的复杂输入规则,可以使用跨字段验证来保持验证逻辑的组织性和可维护性。跨字段验证可确保所有输入值均有效且彼此一致,从而防止出现意外行为。

假设我们有一个表单,用户可以在其中输入任务的开始日期和结束日期,并且我们希望确保结束日期不早于开始日期。我们可以使用跨域验证来实现这一点。

首先,我们定义一个自定义验证注解EndDateAfterStartDate:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EndDateAfterStartDateValidator.class)
public @interface EndDateAfterStartDate {
String message() default “End date must be after start date”;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
然后,我们创建验证器EndDateAfterStartDateValidator:

public class EndDateAfterStartDateValidator implements ConstraintValidator<EndDateAfterStartDate, TaskForm> {
@Override
public boolean isValid(TaskForm taskForm, ConstraintValidatorContext context) {
if (taskForm.getStartDate() == null || taskForm.getEndDate() == null) {
return true;
}

    return taskForm.getEndDate().isAfter(taskForm.getStartDate());
}

}
最后,我们将EndDateAfterStartDate注释应用于我们的表单对象TaskForm:

@EndDateAfterStartDate
public class TaskForm {
@NotNull
@DateTimeFormat(pattern = “yyyy-MM-dd”)
private LocalDate startDate;

@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endDate;

}
现在,当用户提交表单时,验证框架将自动检查结束日期是否晚于开始日期,如果不是,则提供有意义的错误消息。

8 对验证错误使用异常处理

可以使用异常处理ExceptionHandler来统一捕获和处理验证错误。

以下是如何在 Spring Boot 中使用异常处理来处理验证错误的示例:

@RestControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                              HttpHeaders headers, HttpStatus status,
                                                              WebRequest request) {
    Map<String, Object> body = new LinkedHashMap<>();
    body.put("timestamp", LocalDateTime.now());
    body.put("status", status.value());

    // Get all errors
    List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(x -> x.getDefaultMessage())
            .collect(Collectors.toList());

    body.put("errors", errors);

    return new ResponseEntity<>(body, headers, status);
}

}

    在这里,我们创建了一个用 @RestControllerAdvice 注解的 RestExceptionHandler 类来处理我们的 REST API 抛出的异常。然后我们创建一个用@ExceptionHandler注解的方法来处理在验证失败时抛出的 MethodArgumentNotValidException。

    在处理程序方法中,我们创建了一个 Map 对象来保存错误响应的详细信息,包括时间戳、HTTP 状态代码和错误消息列表。我们使用 MethodArgumentNotValidException 对象的 getBindingResult() 方法获取所有验证错误并将它们添加到错误消息列表中。

    最后,我们返回一个包含错误响应详细信息的ResponseEntity对象,包括作为响应主体的错误消息列表、HTTP 标头和 HTTP 状态代码。

    有了这个异常处理代码,我们的 REST API 抛出的任何验证错误都将被捕获并以结构化和有意义的格式返回给用户,从而更容易理解和解决问题。

9 测试你的验证逻辑

需要为你的验证逻辑编写单元测试,以帮助确保它正常工作。

@DataJpaTest
public class UserValidationTest {

@Autowired
private TestEntityManager entityManager;

@Autowired
private Validator validator;

@Test
public void testValidation() {
    User user = new User();
    user.setFirstName("John");
    user.setLastName("Doe");
    user.setEmail("invalid email");

    Set<ConstraintViolation<User>> violations = validator.validate(user);

    assertEquals(1, violations.size());
    assertEquals("must be a well-formed email address", violations.iterator().next().getMessage());
}

}

    我们使用 JUnit 5 编写一个测试来验证具有无效电子邮件地址的“用户”对象。然后我们使用 Validator 接口来验证 User 对象并检查是否返回了预期的验证错误。

10 考虑客户端验证

客户端验证可以通过向用户提供即时反馈并减少对服务器的请求数量来改善用户体验。但是,不应依赖它作为验证输入的唯一方法。客户端验证很容易被绕过或操纵,因此必须在服务器端验证输入,以确保安全性和数据完整性。

总结

有效的验证对于任何 Web 应用程序的稳定性和安全性都是必不可少的。Spring Boot 提供了一套工具和库来简化验证逻辑并使其更易于维护。通过遵循本文中讨论的最佳实践,您可以确保您的验证组件有效并提供出色的用户体验。

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

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

相关文章

跨空间域数据管理分布式共识算法:现状、挑战和展望

跨空间域数据管理分布式共识算法&#xff1a;现状、挑战和展望 李伟明1&#xff0c;李彤1,2, 张大方1&#xff0c;戴隆超1,2, 柴云鹏1,2 1 中国人民大学信息学院&#xff0c;北京 100872 2 数据工程与知识工程教育部重点实验室&#xff0c;北京 100872 摘要&#xff1a;随着数字…

生信学院|09月08日《SOLIDWORKS扣合特征应用》

课程主题&#xff1a;SOLIDWORKS扣合特征应用课程时间&#xff1a;2023年09月08日 14:00-14:30主讲人&#xff1a;陈冬冬 生信科技 售后服务工程师1、什么是扣合特征2、唇缘与凹槽3、装配体凸4、台通风孔5、弹簧扣与弹簧扣卡槽请安装腾讯会议客户端或APP&#xff0c;微信扫描海…

管理类联考——数学——汇总篇——知识点突破——数据分析——计数原理——排列组合——分堆分配

⛲️ 一、考点讲解 1.方法介绍 在排列组合中&#xff0c;经常遇到元素分堆或分组问题&#xff0c;尤其难点是出现等数量的分堆&#xff0c;很多考生容易犯错误。 2.方法应用 平均分成的组&#xff0c;不管他们的顺序如何&#xff0c;都是一种情况&#xff0c;所以分组后一定要…

HTML5-3-表格

文章目录 属性边框属性标题跨行和跨列单元格边距 HTML 表格由 <table> 标签来定义。 tr&#xff1a;tr 是 table row 的缩写&#xff0c;表示表格的一行。td&#xff1a;td 是 table data 的缩写&#xff0c;表示表格的数据单元格。th&#xff1a;th 是 table header的缩…

Python中if __name__ == ‘__main__‘:的作用和原理

if name ‘main’:的作用 一个python文件通常有两种使用方法&#xff0c; 第一是作为脚本直接执行&#xff0c;第二是 import 到其他的 python 脚本中被调用&#xff08;模块重用&#xff09;执行。 举例说明如下&#xff1a; 在本文件中&#xff0c;name 是main 在被impor…

Vue3---uni-app--高德地图引用BUG

先给报错信息&#xff1a;module libs/map//libs/map_min.js is not defined, require args is /libs/map_min.js 查看我引用方法&#xff1a; 本人查阅资料发现 是 require 使用的是 commonJS方式引用说这个适配Vue2可我项目是Vue3应该使用ES6语法糖 然后我有跑了项目发现BU…

【Unity编辑器扩展】 | 编辑器扩展入门基础

前言 【Unity编辑器扩展】 | 编辑器扩展入门基础一、基本概念二、核心知识点 简述三、相关API 总结 前言 当谈到游戏开发工具&#xff0c;Unity编辑器是一个备受赞誉的平台。它为开发者提供了一个强大且灵活的环境&#xff0c;使他们能够创建令人惊叹的游戏和交互式体验。然而…

1.若依框架介绍与环境搭建

文章目录 若依框架介绍官网地址相关技术栈 环境搭建1.git介绍下载与安装学习资料 2.maven介绍与下载环境变量配置资料学习 3.node4.java5.idea6.vscode7.mysql可视化工具HeidiSql 8.redis参考资料遇到问题 若依框架介绍 官网地址 若依框架官网地址&#xff1a;http://www.ruo…

stm32 学习笔记:GPIO输出

一、GPIO简介 引脚电平 0-3.3V,部分可容忍5V&#xff0c;对输出而言最大只能输出3.3V, 只要可以用高低电平来控制的地方&#xff0c;都可以用GPIO来完成&#xff0c;如果控制的功率比较大的设备&#xff0c;只需加入驱动电路即可 GPIO 通用输入输出口&#xff0c;可配置为 8种 …

【程序员必知必会3】你还不懂ClickHouse和Hive的区别?!

ClickHouse和Hive究竟哪些区别 ClickHouse和Hive都是用于大数据处理和分析的分布式存储和计算系统&#xff0c;但它们之间存在一些区别&#xff1a; 架构&#xff1a;ClickHouse采用列式存储和向量化执行引擎&#xff0c;可以实现亚秒级别的数据查询。而Hive采用基于Hadoop的数…

Nancy2.0引入Swagger并设置其为嵌入的资源

因为项目需求&#xff0c;需要在Nancy的基础上引入Swagger&#xff0c;万能的互联网上有现成的方案&#xff0c; 方案写的很详细&#xff0c;实际按文档也成功的实现了相应的功能&#xff0c;但因为我是在基础dll里包含了该功能&#xff0c;所以我希望swagger-ui是作为嵌入的资…

实时操作系统Freertos开坑学习笔记:(七):队列

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、队列是什么&#xff1f;而在freertos中&#xff0c;队列是什么呢&#xff1f;①如果要进行中断、任务的交流&#xff0c;那我用全局变量行吗&#xff1f;②…

VMware虚拟机安装_新虚拟机创建_CentOS镜像导入_linux指令基本操作

文章目录 1 VMware下载安装1.1 下载网址1.2 安装步骤 2 创建虚拟机与CentOS镜像导入2.1 创建新虚拟机2.2 导入CentOS镜像 3 获取ip与连接Xshell3.1 查看虚拟机ip地址3.2 Xshell使用 1 VMware下载安装 1.1 下载网址 https://www.vmware.com/cn/products/workstation-pro/works…

MySQL的虚拟字段

MySQL中的虚拟字段指的是不实际存在于表中的逻辑字段,它们是在查询时由一些函数或表达式临时生成的。 参数&#xff1a;虚拟类型 在 MySQL 中,字段类型可以分为存储类型(Stored)和虚拟类型(Virtual)。存储类型是指实际存储在表中的数据类型,如 INT, VARCHAR, TEXT …

vue3 判断包含某个字符

<img v-if"node.level 1 && checkIfIncludeSubStr(node.label, 人口)"src"/assets/images/icon-convention-01.png" width"16"class"inlineBlock Vmiddle" style"margin-right: 8px;"/>const data reactive…

redis核心数据结构

redis下载地址&#xff1a;Download | Redis linux进入redis目录首先使用make命令进行c的编译&#xff0c;修改redis.conf文件&#xff1a; daemonize yes #后台启动 protected-mode no #关闭保护模式&#xff0c;开启的 # 需要注释掉bind #bind 127.0.0.1&#xff08;bind…

客户案例 | 华苑园林 移动SRM 集成ERP+电子签章,打通数字化最后一公里

公司简介 广州华苑园林股份有限公司始创于1995年&#xff0c;总部设立在广州&#xff0c;是集园林景观设计、工程施工、绿化养护、苗木生产与销售于一体的综合性、跨地区的大型园林企业。拥有城市园林绿化一级施工资质、风景园林设计专项乙级资质、环保工程专业承包叁级资质、…

优先级队列priority_queue以及仿函数的使用

目录 优先级队列priority_queuepriority_queue的模拟实现仿函数 优先级队列priority_queue 优先级队列priority_queue是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它默认第一个元素总是它所包含的元素中最大的 优先级队列默认使用vector作为底层存储数据的…

《Effective软件测试》:让你的软件研发质量提升10倍的秘籍

前言&#xff1a; 软件测试是软件研发过程中不可或缺的一环&#xff0c;它关系到软件的功能、性能、安全和用户体验。然而&#xff0c;很多软件开发者和测试人员对软件测试的理解和实践还存在很多误区和不足&#xff0c;导致软件测试的效率和效果不尽人意&#xff0c;甚至造成软…

Python时间序列分析苹果股票数据:分解、平稳性检验、滤波器、滑动窗口平滑、移动平均、可视化...

全文链接&#xff1a;https://tecdat.cn/?p33550 时间序列是一系列按时间顺序排列的观测数据。数据序列可以是等间隔的&#xff0c;具有特定频率&#xff0c;也可以是不规则间隔的&#xff0c;比如电话通话记录&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。…