Spring Boot 简单有效的数据验证
概要
在现代软件应用的开发中,参数校验对于确保数据的完整性和安全性至关重要。
了不起最近和一个前端实习生联调接口发现,参数校验确实给到前端展示和处理的诸多便利。
Spring Boot作为一个流行的Java框架,提供了多种参数校验的方式。
我们将通过具体的代码实例来演示每种校验方式的用法和效果。
1 参数校验的重要性
在开发过程中,参数校验是确保数据的完整性和安全性的重要环节。
以下是一些原因说明为什么参数校验是必要的:
1.1 数据完整性
参数校验可以防止无效或错误的数据进入系统。
通过校验前端输入的参数,我们可以确保数据的完整性,避免因为缺少必要的信息而导致程序错误或异常。
1.2 安全性
参数校验可以防止恶意用户提交危险的数据。
例如,对于密码字段,我们可以通过校验规则要求用户输入至少8个字符、包含字母和数字等,以增加密码的强度,提高系统的安全性。
1.3 用户体验
参数校验可以帮助我们捕获输入错误,并向用户提供友好的错误提示。
通过及时地反馈错误信息,用户可以更快地发现和纠正输入错误,提升用户体验。
特别是在前后端接口联调时,前端传参错误很快能得到异常提示,就大大提升了联调效率。
2. Spring Boot参数校验的基本原理
在Spring Boot中,参数校验是通过JSR 303规范的Bean Validation实现的。
它基于注解和反射机制,可以轻松地在实体类的字段上进行声明式的校验规则定义。
当请求到达控制器方法时,Spring Boot会自动根据定义的校验规则执行校验操作,并返回校验结果。
2.1. 导入必需的包
在使用参数校验功能时,我们需要导入相关的依赖包。在 pom.xml文件中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
3. 校验方式一:注解方式
注解方式是最常用和方便的参数校验方式。
Spring Boot提供了多种内置的校验注解,包括 @NotNull、@NotBlank、@NotEmpty、@Size、@Pattern和 @Valid等。
3.1. @NotNull、@NotBlank 和 @NotEmpty
@NotNull注解用于检查字段是否为null
@NotBlank注解用于检查字段是否不为空且长度大于0
@NotEmpty注解用于检查字段是否不为空
@Data
public class UserDto {
@NotNull(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@NotEmpty(message = "邮箱不能为空")
private String email;
}
在上述示例中,我们使用了 @NotNull注解来确保 username字段不为null,使用了 @NotBlank注解来确保 password字段不为空且长度大于0,使用了 @NotEmpty注解来确保 email字段不为空。
通过为注解提供错误提示信息,我们可以在校验失败时向用户提供友好的错误提示。
3.2. @Size
@Size注解用于检查字段的长度是否在指定范围内。
@Data
public class UserDto{
@Size(min = 2, max = 20, message = "用户名长度必须在2到20个字符之间")
private String username;
}
在上述示例中,我们使用了 @Size注解来确保 username字段的长度在2到20个字符之间。
如果长度不符合指定范围,校验将失败,并返回指定的错误提示信息。
3.3. @Pattern
@Pattern注解可以用于检查字段是否符合指定的正则表达式。
public class UserDto{
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "用户名只能包含字母和数字")
private String username;
}
在上述示例中,我们使用了 @Pattern注解来确保 username字段只包含字母和数字。
如果字段中包含其他字符,校验将失败,并返回指定的错误提示信息。
3.4. @Valid
@Valid注解用于标记一个嵌套对象,表示需要对该对象进行递归校验。
public class UserDto {
@Valid
private AddressDto address;
}
public class AddressDto {
@NotBlank(message = "地址不能为空")
private String street;
}
在上述示例中,我们使用了 @Valid注解来标记 UserDto中的 AddressDto对象,表示需要对 AddressDto对象进行递归校验。
在校验过程中,会同时校验 AddressDto对象中的 street字段是否为空。
3.5. 复杂逻辑的参数校验
有时候,我们需要对多个字段进行复杂的逻辑校验,例如需要两个字段相互比较或执行自定义的校验逻辑。
在这种情况下,我们可以使用自定义的校验器(Validator)来实现。
public class UserDto {
@NotNull(message = "起始日期不能为空")
private LocalDate startDate;
@NotNull(message = "结束日期不能为空")
private LocalDate endDate;
@AssertTrue(message = "结束日期必须晚于起始日期")
private boolean isEndDateAfterStartDate(){
if (startDate == null || endDate == null) {
return true;
}
return endDate.isAfter(startDate);
}
}
在上述示例中,我们使用了 @AssertTrue注解来标记自定义的校验方法 isEndDateAfterStartDate()。该方法检查 endDate是否晚于 startDate,如果校验失败,将返回指定的错误提示信息。
4. 校验方式二:自定义校验注解
除了使用Spring Boot提供的内置注解外,我们还可以自定义校验注解来满足特定的校验需求。
4.1. 自定义注解
首先,我们需要自定义一个注解,并指定它的校验器。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EmailValidator.class})
public @interface Email {
String message()default "邮箱格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
在上述示例中,我们定义了一个名为 Email的注解,并指定了它的校验器 EmailValidator。
4.2. 自定义校验器
接下来,我们需要实现校验器的逻辑。
public class EmailValidator implements ConstraintValidator<Email, String> {
@Override
public void initialize(Email constraintAnnotation){
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context){
// 在这里编写自定义的校验逻辑
// 返回true表示校验通过,返回false表示校验失败
}
}
在上述示例中,我们实现了 EmailValidator类,并实现了 isValid方法来定义自定义校验逻辑。
4.3. 使用自定义注解
最后,我们可以在实体类中使用自定义注解进行参数校验。
public class UserDto{
@Email
private String email;
}
在上述示例中,我们在 UserDto类的 email字段上使用了自定义的 @Email注解。
当校验发生时,将会调用 EmailValidator中的 isValid方法来执行自定义的校验逻辑。
5. 控制器中的参数校验
在Spring Boot中,我们可以在控制器中使用参数校验来验证请求中的参数。
@RestController
@RequestMapping("/api/users")
public class UserController{
@PostMapping
public ResponseEntity createUser(@Valid @RequestBody UserDto userDto){
// 参数校验通过,执行业务逻辑
}
}
在上述示例中,我们在 createUser方法中使用了 @Valid注解来对 UserDto对象进行参数校验。如果校验失败,将会返回带有错误信息的响应。
6.拓展应用
自定义注解+枚举值
6.1 定义枚举值
package com.bry.advert.enums;
/**
* 回传类型:1有效、2无效、3历史成交、4正向、5负向
*/
public enum FeedbackTypeEnum {
EFFECTIVE_WECHART_ADD_SUCCESS(1, "用户添加企微成功"),
WECHART_FIRST_MESSAGE(2, "微信用户首次信息"),
WECHART_THREE_OPEN(3, "三次开口"),
STOCK_CUSTOMER(4, "股民"),
SMALL_ORDER_PAY(5, "小单产品支付"),
BIG_ORDER_PAY(6, "大单产品支付"),
HISTORY_SMALL_ORDER_PAY(7, "历史已开大单正常客户"),
HISTORY_BIG_ORDER_PAY(8, "历史已开大单正常客户"),
;
private final Integer code;
private final String desc;
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
FeedbackTypeEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 第三方类型
*/
public static PlatformType getEnum(Integer code) {
for (PlatformType enums : PlatformType.values()) {
if (enums.getCode().equals(code)) {
return enums;
}
}
return null;
}
/**
* 校验是否是该枚举-
* **该方法一定不能删除 用于校验参数**
*
* @param code 枚举字符串
* @return true or false
*/
public static boolean isValidEnum(Integer code) {
for (FeedbackTypeEnum myEnum : FeedbackTypeEnum.values()) {
if (myEnum.getCode().equals(code)) {
return true;
}
}
return false;
}
}
6.2自定义参数校验注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueCheck.Validator.class)
public @interface EnumValueCheck {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 枚举类
*/
Class<? extends Enum<?>> enumClass();
/**
* 枚举校验的方法
*/
String enumMethod() default "isValidEnum";
/**
* 默认提示文字
*/
String message() default "enum.value.invalid";
boolean allowNull() default false;
//内部校验器
class Validator implements ConstraintValidator<EnumValueCheck, Object> {
private Class<? extends Enum<?>> enumClass;
private String enumMethod;
private boolean allowNull;
@Override
public void initialize(EnumValueCheck enumValue) {
enumMethod = enumValue.enumMethod();
enumClass = enumValue.enumClass();
allowNull = enumValue.allowNull();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (null == value) {
return allowNull;
}
if (null == enumClass || null == enumMethod) {
return Boolean.TRUE;
}
Class<?> valueClass = value.getClass();
try {
Method method = enumClass.getMethod(enumMethod, valueClass);
if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
throw new BusinessException(BusinessErrorEnum.PARAM_ERROR.getCode(),
String.valueOf(String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass)));
}
if (!Modifier.isStatic(method.getModifiers())) {
throw new BusinessException(BusinessErrorEnum.PARAM_ERROR.getCode(),
String.valueOf(String.format("%s method is not static method in the %s class", enumMethod, enumClass)));
}
Boolean result = (Boolean) method.invoke(null, value);
return result == null ? Boolean.FALSE : result;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new BusinessException(BusinessErrorEnum.PARAM_ERROR.getCode(),
String.valueOf(String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass)));
} catch (NoSuchMethodException | SecurityException e) {
throw new BusinessException(BusinessErrorEnum.PARAM_ERROR.getCode(),
String.valueOf(String.format("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass)),
e);
}
}
}
}
6.3 自定义注解的使用
@Data
public class FeedbackResourceRequest {
@ApiModelProperty(value = "unionId 唯一", required = true)
@NotNull(message = "unionId 不能为空")
private String unionId;
@ApiModelProperty(value = "回传类型:1有效、2无效、3历史成交、4正向、5负向", required = true)
@EnumValueCheck(enumClass = FeedbackTypeEnum.class, message = "邀请类型错误")
private Integer feedbackType;
}
结论
Spring Boot中参数校验的多种实现方式,包括注解方式和自定义校验注解方式。
我们通过具体的代码示例演示了每种校验方式的用法和效果。
相信你肯定能应用好参数校验,将一些简单的逻辑校验使用注解,稍稍复杂的使用校验器,复杂业务的校验可以再具体的业务代码中实现。