Spring注解魔法:集合中对象属性唯一性验证的完美解决方案

Spring Boot提供了强大的校验框架,但有时我们需要根据自己的业务需求创建自定义的校验规则。本文将介绍如何使用Spring Boot自定义注解、校验器以及反射来检查集合中每个对象的某一属性的值是否唯一。

一、自定义注解

  • 创建注解:@UniqueProperty用于集合中每一个对象元素的某个相同字段进行值的唯一性校验

    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import static java.lang.annotation.ElementType.*;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    /**
     * 用于集合中每一个对象元素的某个相同字段进行值的唯一性校验
     */
    @Target({PARAMETER, FIELD}) // 指定适用对象
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = UniquePropertyValidator.class) // 指定校验器类
    public @interface UniqueProperty {
    
        // 默认0: 集合中的对象的第一个字段
        int index() default 0;
        
        // 提示信息
        String message() default "value is notUnique";
        
        // 继续定义其他...
        //Boolean canNull();
        
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    }
    

    这个注解可以适合用在类的集合类型的字段方法的集合参数上,用来标识集合中的对象的第index个位置的字段需要做唯一性校验

二、定义校验类

  • 创建校验器类 NotNullFieldValidator,实现ConstraintValidator接口,并重写initializeisValid方法。
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.util.CollectionUtils;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.lang.reflect.Field;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Set;
    
    @Slf4j
    public class UniquePropertyValidator implements ConstraintValidator<UniqueProperty , Collection<?>> {
    
        private int index;
    
        // 初始化方法
        @Override
        public void initialize(UniqueProperty constraintAnnotation) {
            // 获取注解的 index 字段的值
            index = constraintAnnotation.index();
        }
    
        // 校验逻辑
        @Override
        public boolean isValid(Collection<?> objects, ConstraintValidatorContext context) {
            if (CollectionUtils.isEmpty(objects)) {
                return false;
            }
    
            // 存放集合的每个对象的index处所获取的字段的值
            Set<Object> uniqueValues = new HashSet<>();
            /*
             * 遍历对象数组,即注解作用的对象
             * 例:
             *      @UniqueProperty(index = 2)
             *      private List<User> list;
             *      isValid方法中的参数 objects 指的就是 list
             * */
            for (Object obj : objects) {
                //获取指定的 index处的属性值
                Object param = getFieldValue(obj, index);
                if (param == null || uniqueValues.contains(param)) {
                    return false;
                }
                uniqueValues.add(param);
            }
            return true;
        }
    
        //获取指定的 index处的字段值
        public static Object getFieldValue(Object object, int index) {
            // 反射 获取对象的字段(Field)集合
            Field[] fields = object.getClass().getDeclaredFields();
            if (fields.length == 0) {
                throw new IllegalArgumentException("Object has no fields.");
            }
            try {
                // 获取字段名
                String fieldName = fields[index].getName();
                // 获取具有指定名称 fieldName 的字段对象
                Field field = object.getClass().getDeclaredField(fieldName);
                // 暴力破解(包括私有字段)
                field.setAccessible(true);
                Object fieldValue = field.get(object);
                log.info("属性值唯一性校验: {}的值为{}", fieldName, fieldValue);
                return fieldValue;
            } catch (NoSuchFieldException | IllegalAccessException e) {
                log.warn("属性值唯一性校验异常: {}", e.getMessage());
                return null;
            }
        }
    }
    

三、测试我们的注解

1. 注意:在pom.xml文件中添加下面的依赖

  • 用于集成和自动配置Java Bean Validation(JSR 380)框架。
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

2. 测试-作用在方法参数

  • 在未使用javax.validation.Validator来执行校验的情况下,校验并不会自动触发。

  • 这里我们可以在不显式调用它的情况下手动触发校验:在MyServicetestUniqueAnnotation方法参数上使用@UniqueProperty注解,并通过@Validated注解告诉Spring Boot执行校验。如果collection中的对象的字段不满足@UniqueProperty的条件,将抛出ConstraintViolationException异常。

    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.validation.annotation.Validated;
    
    import java.util.Collection;
    
    @Data
    @Service
    @Slf4j
    @Validated // 通过@Validated注解告诉Spring Boot执行校验
    public class MyService {
    
        public Boolean testUniqueAnnotation(@UniqueProperty(index = 2, message = "idNumber is not unique") Collection<?> collection) {
            // 在方法参数上使用 @UniqueProperty 注解
            // 如果参数 collection中的对象的字段不满足 @UniqueProperty 的条件,将会抛出 ConstraintViolationException 异常
            log.info("Collection content: {}", collection);
            return true;
        }
    }
    
  • 编写Schoolfellow类

    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class Schoolfellow {
        private String name;
        private Integer age;
        private String idNumber;
    }
    
  • 编写测试单元,调用MyService中的testUniqueAnnotation方法。

    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import javax.annotation.Resource;
    import java.util.ArrayList;
    import java.util.Collection;
    
    
    @SpringBootTest
    public class UniquePropertyTest {
    
        @Resource
        private MyService myService;
    
        @Test
        public void annotationTest1() {
            Collection<Schoolfellow> userList = new ArrayList<>();
            userList.add(new Schoolfellow("zhangsan",18,"123456789"));
            userList.add(new Schoolfellow("lisi",21,"123456"));
            userList.add(new Schoolfellow("zhangsan",23,"123456"));
            myService.testUniqueAnnotation(userList);
        }
    
    }
    
  • 测试不通过的异常日志

    测试异常图.png

3. 测试-作用在类的字段上,

  • 编写School类,在SchoolList字段上使用@UniqueProperty注解。

    import lombok.Data;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    
    @Data
    @Component
    public class School {
        @UniqueProperty(index = 0, message = "name is not unique")
        private Collection<Schoolfellow> List;
    
    }
    
  • MyService中添加testUniqueAnnotationByValid方法,使用 @Valid 注解来标记需要校验的类,然后在方法参数中使用这个类(School)。

    public Boolean testUniqueAnnotationByValid(@Valid School school) {
        // 在方法参数上使用 @Valid 注解
        log.info("Collection content: {}", school.getList());
        return true;
    }
    
    1. 编写测试单元,调用MyService中的testUniqueAnnotationByValid方法。
    @Resource
    private School school;
    
    @Test
    public void annotationTest2() {
        Collection<Schoolfellow> userList = new ArrayList<>();
        userList.add(new Schoolfellow("zhangsan",18,"123456789"));
        userList.add(new Schoolfellow("lisi",21,"123456"));
        userList.add(new Schoolfellow("zhangsan",23,"123456"));
        school.setList(userList);
        myService.testUniqueAnnotationByValid(school);
    }
    
  • 测试不通过的异常日志

    侧视图2.png

四、特别说明

  • @Valid 是一个Java注解,通常用于标记在方法参数、方法返回值、字段、方法、构造函数等位置,它的主要作用是告诉Bean Validation(JSR 380规范中定义的Java Bean验证框架)在执行验证时,应该递归验证标记为 @Valid 的对象。

    具体来说,@Valid 的作用如下:

    1. 方法参数上使用 @Valid:在方法参数上使用 @Valid 注解时,Bean Validation会自动递归验证参数中的嵌套对象。这对于验证复杂对象的属性非常有用,以确保嵌套对象中的属性也受到验证。
    public void createUser(@Valid User user) {
        // 验证User对象及其属性
    }
    
    1. 方法返回值上使用 @Valid:还可以在方法返回值上使用 @Valid 注解,以确保返回的对象经过验证。这对于确保服务方法返回的对象是有效的非常有用。
    @Valid
    public User getUser() {
        // 返回User对象
    }
    
    1. 字段上使用 @Valid:虽然不太常见,但也可以在类的字段上使用 @Valid 注解,通常在嵌套对象的情况下。这将告诉Bean Validation验证字段的值。
    public class Order {
        @Valid
        private ShippingAddress shippingAddress;
    
        // Getters and setters
    }
    

    总之@Valid 注解主要用于在Bean Validation框架中执行嵌套验证,以确保验证递归到标记为 @Valid 的对象的属性,以及在返回值上使用它来确保方法返回的对象经过验证。这有助于确保应用程序中的数据完整性和一致性。

除此之外:还可以在注解中继续添加一些字段,增加校验规则,比如:在注解内添加一个Boolean canNull()用以规定字段的值是否能为空,然后在校验类获取canNull进行判断。确保在实际应用中适当地处理异常和错误情况,以满足我们的实际开发需求。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值