likes
comments
collection
share

利用自定义注解EnumValue在Spring中优雅地校验枚举值

作者站长头像
站长
· 阅读数 24

开发中,数据校验是确保数据质量和应用程序稳定性的关键步骤。Java Bean Validation API(JSR 380)提供了一个标准的方法来校验对象的属性。然而,有时需要对特定属性进行更复杂的校验,例如检查一个字符串是否为有效的枚举值。为了解决这类问题,可以创建自定义校验注解。本文将详细介绍如何实现一个名为 EnumValue 的自定义校验注解,它用于校验字符串是否为指定枚举类型的有效枚举常量。

背景

在Java中,枚举(Enum)是表示一组固定常量的一种类型。在处理表单提交或者JSON序列化时,经常需要确保客户端提供的字符串值能够映射到有效的枚举常量上。Java Bean Validation API提供了基本的校验机制,但没有直接支持枚举值的校验。这就是需要自定义注解 EnumValue 的原因。

EnumValue 注解

EnumValue 是一个自定义注解,用于校验字符串是否为枚举类中定义的一个有效值。它可以被用于任何字符串字段,并且可以指定是否对大小写敏感,或者是否需要在校验前转换大小写格式。

以下是 EnumValue 注解的定义:

EnumValue 是一个自定义的注解,用于校验 Java 字段值是否为某个枚举类型的有效枚举常量。这个注解是基于 Java Bean Validation API(JSR 380)的一个扩展,它允许为字段添加额外的校验规则。该注解通过 ConstraintValidator 接口的实现类 Validator 来执行具体的校验逻辑。### 作用和特性:

  1. @Documented: 表明这个注解应该被 javadoc 工具记录。
  2. @Constraint(validatedBy = EnumValue.Validator.class): 指定这个注解的校验器为内部类 Validator
  3. @Target(ElementType.FIELD): 表明这个注解只能用于字段。
  4. @Retention(RetentionPolicy.RUNTIME): 表明这个注解在运行时保留,因而可以通过反射读取。
  5. @ReportAsSingleViolation: 表明如果这个注解的校验失败,应该被视为单一的约束违规。
  6. message(): 提供默认的错误消息模板,当校验失败时使用。
  7. groups(): 允许指定校验组,用于分组校验。
  8. payload(): 用于客户端指定校验负载。
  9. value(): 指定要校验的枚举类。
  10. convertCase(): 表明是否在校验前对枚举名称和输入值进行大小写转换,默认为 true
  11. from()to(): 指定大小写转换的格式,使用了 Guava 库的 CaseFormat 枚举。

Validator 内部类的作用:

Validator 类实现了 ConstraintValidator 接口,定义了校验逻辑:

  • initialize: 初始化方法,接收并保存注解的实例。
  • isValid: 校验方法,检查给定的字符串是否是有效的枚举常量。如果 convertCasetrue,它会将枚举常量名称从 from 指定的格式转换到 to 指定的格式,然后和输入值进行比较。
import com.google.common.base.CaseFormat;
import org.apache.commons.lang3.StringUtils;

import javax.validation.*;
import java.lang.annotation.*;
import java.util.Arrays;

/**
 * @Author derek_smart
 * @Date 2024/6/28 8:00
 * @Description  校验 Java 字段值是否为某个枚举类型的有效枚举常量
 */
@Documented
@Constraint(validatedBy = EnumValue.Validator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@ReportAsSingleViolation
public @interface EnumValue {
    String message() default "枚举值不正确";

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

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

    Class<? extends Enum> value();

    boolean convertCase() default false;

    CaseFormat from() default CaseFormat.UPPER_UNDERSCORE;

    CaseFormat to() default CaseFormat.LOWER_CAMEL;

    class Validator implements ConstraintValidator<EnumValue, String> {
        private EnumValue enumValue;

        @Override
        public void initialize(EnumValue enumValue) {
            this.enumValue = enumValue;
        }

        @Override
        public boolean isValid(String text, ConstraintValidatorContext constraintValidatorContext) {
            if (StringUtils.isEmpty(text)) {
                return true;
            }
            return Arrays.stream(enumValue.value().getEnumConstants())
                    .anyMatch(e -> {
                        String name = e.name();
                        if (enumValue.convertCase()) {
                            name = enumValue.from().to(enumValue.to(), name);
                        }
                        return name.equals(text);
                    });
        }
    }
}

EnumValue.Validator 内部类

EnumValue.Validator 是实现 ConstraintValidator 接口的内部类,负责具体的校验逻辑。它会检查提供的字符串是否为注解中指定的枚举类的一个有效枚举常量。 如果 convertCase 属性为 true,它还会根据 fromto 属性指定的格式转换大小写。

以下是 Validator 类的简化实现:

public class Validator implements ConstraintValidator<EnumValue, String> {
    // 实现省略...
}

使用 EnumValue 注解

假设有一个名为 Status 的枚举,其中定义了几种状态:

public enum Status {
    ACTIVE,
    INACTIVE,
    PENDING;
}

可以在实体类中使用 EnumValue 注解来校验字段:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotNull;

/**
 * @Author derek_smart
 * @Date 2024/6/28 8:00
 * @Description  测试用户类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @NotNull(message = "状态不能为空")
    @EnumValue(value = Status.class, message = "状态值不正确")
    private String status;

    private String name;
}

在这个例子中,status 字段必须是 Status 枚举中的一个有效值。如果提供的值不在枚举中,校验将失败,并且会抛出一个校验异常。

集成和校验

要使 EnumValue 注解生效,需要配置一个支持 Bean Validation 的环境。在Spring框架中,这通常意味着在Spring Boot应用中包含相应的依赖,并且在实体类或DTO类上使用注解。

当创建或更新 User 实例时,如果 status 字段的值不是 Status 枚举中的一个有效值,校验将失败,可以捕获异常并处理错误。

import org.hibernate.validator.HibernateValidator;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class UserValidationTest {
    private static Validator validator;

    static {
        ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .buildValidatorFactory();
        validator = factory.getValidator();
    }

    public static void main(String[] args) {
        // 创建一个有效的 User 实例
        User validUser = new User("ACTIVE","张三");

        // 创建一个无效的 User 实例(状态值不是枚举中的一个)
        User invalidUser = new User("UNKNOWN","李四");

        // 校验有效的 User 实例
        Set<ConstraintViolation<User>> validViolations = validator.validate(validUser);
        printViolations(validViolations);

        // 校验无效的 User 实例
        Set<ConstraintViolation<User>> invalidViolations = validator.validate(invalidUser);
        printViolations(invalidViolations);
    }

    private static void printViolations(Set<ConstraintViolation<User>> violations) {
        if (violations.isEmpty()) {
            System.out.println("No violations. The object is valid.");
        } else {
            violations.forEach(violation -> System.out.println(violation.getMessage()));
        }
    }
}

利用自定义注解EnumValue在Spring中优雅地校验枚举值UserValidationTest 类中,使用 Hibernate Validator 作为 JSR 380 的实现来校验 User 实例。创建了一个有效的 User 实例(其 status 字段值为 "ACTIVE",这是枚举 Status 中的一个有效值),以及一个无效的 User 实例(其 status 字段值为 "UNKNOWN",这不是枚举 Status 中的一个值)。

当调用 validator.validate() 方法时,它会校验 User 实例的 status 字段。如果 status 字段的值不是 Status 枚举中的一个有效值,校验将失败,并且会打印出错误消息 "状态值不正确"。

实战应用:

UserController中,定义了一个createUser方法,它接收一个User对象作为请求体。使用@Valid注解来触发对User对象的校验。如果客户端提交的status值不是Status枚举中的有效值,校验将失败,并且客户端将收到一个包含错误信息的响应。

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/users")
@Validated
public class UserController {

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody User user) {
        // 处理用户数据,例如保存到数据库
        // ...
        return ResponseEntity.ok("用户创建成功,状态:" + user.getStatus());
    }


}

确保的Spring Boot应用已经配置了异常处理器来处理校验失败的情况:

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 org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.http.HttpStatus;

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public String handleValidationExceptions(MethodArgumentNotValidException ex) {
        return ex.getBindingResult().getFieldErrors().stream()
                .map(fieldError -> fieldError.getDefaultMessage())
                .findFirst()
                .orElse("校验错误");
    }
}

现在,当客户端尝试创建一个新的User对象时,如果提交的status值不是ACTIVEINACTIVEPENDING中的一个,客户端将收到一个400 Bad Request响应,其中包含了EnumValue注解中定义的错误信息"无效的状态值"

总结

通过创建 EnumValue 注解,为Java开发者提供了一个强大的工具,以确保字符串值符合预定义的枚举常量。 这样的自定义校验注解极大地提升了数据的准确性和代码的健壮性。无论是在简单的表单提交还是在复杂的业务逻辑中,EnumValue 都能确保只有正确的枚举值被接受和处理。

转载自:https://juejin.cn/post/7385778376276197417
评论
请登录