🎯导读:本文档介绍了参数校验的重要性及其在软件开发中的作用,强调了数据完整性、安全性、用户体验、系统稳定性及开发效率等方面的关键价值。文档详细阐述了Hibernate Validator这一流行的Java验证框架的使用方法,展示了如何利用其内置注解(如@NotNull、@Size、@Email等)来对输入数据进行有效性检查。此外,还探讨了自定义校验规则的开发方式,以及如何通过分组校验来适应不同的业务场景需求。通过集成Hibernate Validator,开发者可以显著提升应用程序的质量与用户体验。
文章目录
- 参数校验
- Hibernate Validator 简介
- 依赖
- 基础使用
- 常用校验注解
- 字段校验注解
- 其他注解
- 分组校验
- 定义分组
- 使用
- 自定义校验
- 定义注解
- 校验器实现
- 使用
- 排班系统实现(以添加节日为例)
- 代码位置
- 参数类
- 接口
- 统一异常处理
- 测试
- 嵌套校验
参数校验
参数校验是指在接收输入数据时,对传入的参数进行验证,以确保它们符合预期的格式、范围和有效性。这种校验对于保证软件的稳定性和安全性至关重要。以下是参数校验的一些关键作用和意义:
- 数据完整性:
- 参数校验可以帮助确保接收到的数据符合预期的格式和结构。例如,通过校验确保日期格式正确、数值在合理范围内、字符串长度合适等。
- 这样可以防止因输入数据错误而导致的程序异常或错误行为。
- 安全性:
- 校验可以防止恶意或意外的数据注入,如 SQL 注入、XSS 攻击等。通过严格的校验规则,可以降低这些安全风险。
- 正确的校验机制能够帮助过滤掉不安全的输入,保护系统免受攻击。
- 用户体验:
- 在用户界面上,及时反馈错误信息给用户,帮助他们更快地纠正输入错误,改善用户体验。
- 清晰的错误提示可以使用户更容易理解和操作系统,减少因输入错误造成的困惑和沮丧感。
- 系统稳定性:
- 通过在校验阶段捕获潜在的问题,可以提前处理错误情况,避免后续处理逻辑中出现未预见的异常。
- 这有助于确保系统在面对各种输入时都能保持一致的行为,提高系统的可靠性和稳定性。
- 开发效率:
- 自动化的参数校验减少了手工检查的需要,简化了开发过程,降低了出错的可能性。
- 开发者可以专注于核心业务逻辑,而不必担心基础的数据验证问题。
- 易于维护:
- 明确的校验规则使得代码更易于理解和维护。当需要调整业务逻辑或数据格式时,可以在一处修改校验逻辑即可。
- 这有助于保持代码的清晰和整洁,便于团队协作。
Hibernate Validator 简介
参数校验最直接的方式就是在方法中实现具体的逻辑来判断参数是否合理,但是如果每个方法都要这样判断,未免太过复杂,Hibernate Validator 定义了一些常用的校验注解可以帮助我们快速搞定一些常见的常数校验,如非空、长度限制、邮件是否合格……
Hibernate Validator 是一个强大的开源库,用于实现 Java Bean Validation(JSR 303 和 JSR 380)规范。它是 Java 应用程序中最广泛使用的验证框架之一。Hibernate Validator 提供了一套丰富的约束注解,允许开发者轻松地对实体对象中的属性进行约束定义,确保它们满足特定的业务规则。通过使用如 @NotNull
、@Size
、@Pattern
等内置注解,开发者可以方便地检查对象的状态,确保数据的完整性和一致性。
Hibernate Validator 不仅支持标准的 JSR 规范中定义的约束,还提供了一些额外的注解,如 @Length
和 @ScriptAssert
,使得验证逻辑更加灵活和强大。此外,它还支持自定义约束注解的开发,允许根据具体的应用需求来扩展验证功能。通过集成 Hibernate Validator,开发者可以在运行时自动执行验证逻辑,减少手工编写验证代码的工作量,并提高代码的可读性和可维护性。
Hibernate Validator 的应用场景非常广泛,从简单的 Web 表单数据验证到复杂的业务规则检查,都可以看到它的身影。无论是前端还是后端开发,Hibernate Validator 都能帮助开发者确保数据的准确性和有效性,从而提升软件的质量和用户体验。
依赖
在父工程添加如下依赖来管理版本
<hibernate-validator.version>6.2.5.Final</hibernate-validator.version>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
在sss-common
、sss-enterprise
、sss-aggregation
模块添加都如下依赖,你可能会疑惑,sss-enterprise
模块不是已经导入了sss-common
了吗,为什么还要重复引用。亲测不引用,参数会生效,这个bug我找了一个下午o(╥﹏╥)o
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
基础使用
首先给参数类的字段添加注解,例如给name定义非空检验
@Data
@TableName("festival")
@NoArgsConstructor
@AllArgsConstructor
public class FestivalEntity extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节日名称
*/
@NotBlank(message = "节日名称不能为空")
private String name;
/**
* 起始日期
*/
@NotNull(message = "起始日期不能为空")
private Date startDate;
/**
* 截止日期
*/
@NotNull(message = "截止日期不能为空")
private Date endDate;
/**
* 门店id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long storeId;
/**
* 0:农历 1:新历
*/
private int type;
}
接在给Controller的方法添加校验注解,@Validated要添加在@RequestBody前面,这样接收到参数festival之后,就会按照所设定规则对里面的字段值进行校验
@PostMapping("/save")
public R save(@Validated @RequestBody FestivalEntity festival, HttpServletRequest httpServletRequest) {
long storeId = Long.parseLong(JwtUtil.getStoreId(httpServletRequest.getHeader("token")));
festival.setStoreId(storeId);
festivalService.save(festival);
return R.ok();
}
常用校验注解
字段校验注解
【布尔类】
- **
@AssertTrue
**布尔表达式必须为true。 - **
@AssertFalse
**布尔表达式必须为false。
@AssertTrue(message = "你必须同意服务条款")
private boolean agreeToTerms;
@AssertFalse(message = "您必须不是机器人")
private boolean isRobot;
【数字类】
- **
@Min
**数值必须大于等于指定的最小值。 - **
@Max
**数值必须小于等于指定的最大值。 - **
@DecimalMin
**十进制数必须大于等于指定的最小值。 - **
@DecimalMax
**十进制数必须小于等于指定的最大值。 - **
@Digits
**检查数字的整数部分和小数部分的位数是否不超过指定的值。 - **
@Range
**数字或日期必须在指定范围内。 - **
@Negative
**数字必须是负数。 - **
@NegativeOrZero
**数字必须是负数或零。 - **
@Positive
**数字必须是正数。 - **
@PositiveOrZero
**数字必须是正数或零。
// 确保年龄至少为 18 岁
@Min(value = 18)
private int age;
// 确保考试分数不能超过 100 分
@Max(value = 100)
private int score;
// 确保价格至少为 10.00。inclusive = true 表示包括 10.00 在内
@DecimalMin(value = "10.00", inclusive = true)
private BigDecimal price;
// 确保折扣率不能超过 0.99。inclusive = false 表示不包括 0.99
@DecimalMax(value = "0.99", inclusive = false)
private BigDecimal discountRate;
@Digits(integer = 10, fraction = 2, message = "交易金额的整数部分不能超过10位数,小数部分不能超过2位数")
private BigDecimal amount;
@Digits(integer = 5, fraction = 2, message = "手续费的整数部分不能超过5位数,小数部分不能超过2位数")
private BigDecimal fee;
@Range(min = 18, max = 100, message = "年龄必须在18到100岁之间")
private int age;
@Negative(message = "温度必须低于零度")
private double temperature;
@NegativeOrZero(message = "海拔高度必须是负数或零")
private int altitude;
@Positive(message = "体重必须是正数")
private double weight;
@PositiveOrZero(message = "身高必须是正数或零")
private double height;
【日期类】
- **
@Past
**日期必须在过去。 - **
@Future
**日期必须在未来。 @PastOrPresent
:用于验证日期是否在过去或当前日期。@FutureOrPresent
:用于验证日期是否在未来或当前日期。
@Past(message = "出生日期必须在过去")
private LocalDate birthDate;
@Future(message = "预约日期必须在未来")
private LocalDate appointmentDate;
@PastOrPresent
private LocalDate lastLogin;
@FutureOrPresent
private LocalDate nextAppointment;
【字符串类】
- **
@NotBlank
**字符串不能为null且去除空白后长度必须大于零。 - **
@Length
**字符串长度必须在指定范围内。 - **
@Email
**字符串必须是有效的电子邮件地址。 - **
@Pattern
**字符串必须匹配正则表达式。 - **
@CreditCardNumber
**字符串必须是一个有效的信用卡号。
@NotBlank(message = "评论内容不能为空")
@Length(min = 5, max = 200, message = "评论内容长度必须在5到200个字符之间")
private String content;
@Email(message = "电子邮件地址无效")
private String email;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", message = "密码必须包含字母和数字,且长度至少为8个字符")
private String password;
@CreditCardNumber(message = "信用卡号无效")
private String creditCardNumber;
【通用】
- **
@NotEmpty
**字符串、集合、数组等不能为null且长度必须大于零。 - **
@NotNull
**字段或属性不能为null。 - **
@Null
**字段或属性必须为null。 - **
@Size
**用于字符串、集合、数组等,检查大小是否在指定范围内。
@NotNull(message = "用户名不能为空")
private String username;
@NotNull(message = "密码不能为空")
private String password;
@Size(min = 3, max = 20, message = "用户名长度必须在3到20个字符之间")
private String username;
@Size(min = 1, message = "至少需要选择一项兴趣")
private List<String> interests;
@NotEmpty(message = "用户名不能为空")
private String username;
其他注解
- **
@Constraint
**用于创建自定义约束。 - **
@ConstraintValidator
**用于创建自定义约束的验证器。 - **
@ConstraintViolation
**用于描述约束违反的情况。 - **
@ConstraintDescriptor
**用于描述约束元数据。 - **
@ValidationGroups
**允许你对验证逻辑进行分组,从而控制哪些约束在何时被应用。 - **
@Valid
**应用于对象,验证对象的所有属性。 - **
@Validated
**通常用于框架集成,比如Spring,以启用方法参数验证。
分组校验
同一个字段,不同方法中可能对其有不同的要求。例如名字这个字段,添加用户的时候要求该字段非空,但修改用户信息的时候,如果不修改名字,该字段可以为空。
为了满足上面的需求,我们需要定义不同的小组来进行校验隔离
定义分组
新增分组
/**
* @Author dam
* @create 2024/8/31 10:32
*/
public interface AddGroup {
}
修改分组
/**
* @Author dam
* @create 2024/8/31 10:32
*/
public interface UpdateGroup {
}
使用
在使用注解的时候,使用groups属性来定义即可
@NotNull(message = "起始日期不能为空", groups = {AddGroup.class})
private Date startDate;
@NotNull(message = "起始日期不能为空", groups = {AddGroup.class, UpdateGroup.class})
private Date startDate;
自定义校验
自定义校验规则是为了让我们可以定义一些项目通用,但是Hibernate Validator不具备的校验方法。下面的例子会实现一个校验 传入参数 是否被 指定数组 所包含,例如参数是被在{1,2,3,4,5}当中
定义注解
import com.dam.valid.validator.TypeValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 注解:校验是否在所包含的数字中
*
* @Author dam
* @create 2024/8/31 10:48
*/
@Documented
@Constraint(validatedBy = {TypeValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeAnno {
//--------------- 必须包含字段 ----------------
String message() default "字段必须是0或1";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
//--------------- 自定义字段 ----------------
/**
* 用来接收所包含的值
**/
int[] values() default {};
}
@Documented
:当你生成Javadoc文档时,该注解将被记录下来,方便其他开发者查阅@Constraint(validatedBy = {TypeValidator.class})
:表示这是一个验证约束注解,并指定了一个验证器类TypeValidator
,用于执行具体的验证逻辑。 validatedBy属性用来指定一个或多个验证器类,这些类负责实现具体的验证逻辑@Target
:定义此注解可以应用的目标元素类型。ElementType.METHOD
:方法。ElementType.FIELD
:字段。ElementType.CONSTRUCTOR
:构造函数。ElementType.PARAMETER
:方法或构造函数的参数。
@Retention(RetentionPolicy.RUNTIME)
:定义了注解的保留策略,这里设置为RUNTIME
,意味着该注解将在编译时保留,并且可以在运行时通过反射访问。
校验器实现
import com.dam.valid.annotations.TypeAnno;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* 校验器:校验是否在所包含的数字中
*
* @Author dam
* @create 2024/8/31 10:52
*/
public class TypeValidator implements ConstraintValidator<TypeAnno, Integer> {
/**
* 存储类型
*/
private Set<Integer> typeSet = new HashSet<>();
@Override
public void initialize(TypeAnno constraintAnnotation) {
for (int value : constraintAnnotation.values()) {
typeSet.add(value);
}
}
/**
* 校验字段是否有效
*
* @param value 要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
System.out.println("触发校验");
return typeSet.contains(value);
}
}
使用
/**
* 0:农历 1:新历
*/
@TypeAnno(values = {0, 1}, groups = {AddGroup.class}, message = "节日日期类型只能是农历(0)、新历(1)")
private int type;
排班系统实现(以添加节日为例)
代码位置
参数类
import com.baomidou.mybatisplus.annotation.TableName;
import com.dam.model.entity.BaseEntity;
import com.dam.valid.annotations.TypeAnno;
import com.dam.valid.groups.AddGroup;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
* 门店节日表
*
* @author dam
* @email 1782067308@qq.com
* @date 2023-03-13 16:42:08
*/
@Data
@TableName("festival")
@NoArgsConstructor
@AllArgsConstructor
public class FestivalEntity extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节日名称
*/
@NotBlank(message = "节日名称不能为空")
private String name;
/**
* 起始日期
*/
@NotNull(message = "起始日期不能为空", groups = {AddGroup.class})
private Date startDate;
/**
* 截止日期
*/
@NotNull(message = "截止日期不能为空", groups = {AddGroup.class})
private Date endDate;
/**
* 门店id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long storeId;
/**
* 0:农历 1:新历
*/
@TypeAnno(values = {0, 1}, groups = {AddGroup.class}, message = "节日日期类型只能是农历(0)、新历(1)")
private int type;
}
接口
/**
* 保存
*/
@PostMapping("/save")
@OperationLog(title = FestivalController.title, businessType = BusinessTypeEnum.INSERT, detail = "新增节日")
@PreAuthorize("hasAuthority('bnt.festival.add')")
public R save(@Validated({AddGroup.class}) @RequestBody FestivalEntity festival, HttpServletRequest httpServletRequest) {
long storeId = Long.parseLong(JwtUtil.getStoreId(httpServletRequest.getHeader("token")));
festival.setStoreId(storeId);
festivalService.save(festival);
return R.ok();
}
统一异常处理
当请求参数未能通过 Spring MVC 的数据绑定和校验时,会抛出 MethodArgumentNotValidException。该方法捕捉此类异常,并将校验错误信息整理后返回给客户端。
import com.dam.model.enums.ResultCodeEnum;
import com.dam.model.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 参数校验异常
* @param exception
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public R handleValidException(MethodArgumentNotValidException exception) {
// 创建一个 Map 用来存储每个字段的错误信息
Map<String, String> map = new HashMap<>();
// 获取数据校验的错误结果
BindingResult bindingResult = exception.getBindingResult();
// 遍历所有的 FieldError 对象,将每个字段的错误信息存入 map 中
bindingResult.getFieldErrors().forEach(fieldError -> {
// 获取错误信息
String message = fieldError.getDefaultMessage();
// 获取字段名
String field = fieldError.getField();
// 将字段名和错误信息放入 map
map.put(field, message);
});
// 记录错误日志
log.error("数据校验出现问题{}, 异常类型{}", exception.getMessage(), exception.getClass());
// 获取错误信息并格式化成字符串
StringBuilder sb = new StringBuilder();
int id = 1;
for (String key : map.keySet()) {
// 格式化错误信息,每条错误信息前加上编号
sb.append(id++).append(") ").append(key).append("->").append(map.get(key)).append("<br/>");
}
// 打印错误信息到控制台
System.out.println(sb.toString());
// 返回包含错误代码和错误信息的 R 对象
return R.error(ResultCodeEnum.ARGUMENT_VALID_ERROR.getCode(), ResultCodeEnum.ARGUMENT_VALID_ERROR.getMessage() + ":<br/>" + sb.toString());
}
}
测试
使用ApiFox软件来进行接口测试
定义请求头参数
发送请求
嵌套校验
嵌套校验指在进行数据验证时,不仅仅检查顶层对象的属性是否符合预期,同时也深入到对象的子对象或者集合中去验证它们的属性是否满足特定规则的过程。这种校验方式常见于复杂的数据结构中,比如对象的属性可能是一个列表,而列表中的每个元素本身又是一个对象;或者是对象的某个属性是一个具有多个字段的另一个对象。这时需要使用@Valid
来修饰该属性
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
// 订单实体类
public class Order {
@NotNull(message = "用户信息不能为空")
@Valid
private User user;
@NotEmpty(message = "订单中至少需要包含一个产品")
@Valid
private List<Product> products;
// Getters and Setters
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<Product> getProducts() {
return products;
}
public void setProducts(List<Product> products) {
this.products = products;
}
}
// 用户实体类
public class User {
@NotNull(message = "用户名不能为空")
private String name;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}