Spring annotation magic: the perfect solution for uniqueness verification of object properties in collections

Spring Boot provides a powerful validation framework, but sometimes we need to create custom validation rules according to our own business needs. This article will introduce how to use Spring Boot custom annotations, validators, and reflection to check whether the value of a certain attribute of each object in the collection is unique.

1. Custom annotations

  • Create annotation: @UniqueProperty is used to verify the uniqueness of the same field of each object element in the collection.

    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;
    
    /**
     * Used to verify the uniqueness of a certain same field of each object element in the collection.
     */
    @Target({<!-- -->PARAMETER, FIELD}) // Specify the applicable object
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = UniquePropertyValidator.class) //Specify the validator class
    public @interface UniqueProperty {<!-- -->
    
        //Default 0: the first field of the object in the collection
        int index() default 0;
        
        // Prompt information
        String message() default "value is notUnique";
        
        // Continue to define other...
        //Boolean canNull();
        
        Class<?>[] groups() default {<!-- --> };
    
        Class<? extends Payload>[] payload() default {<!-- --> };
    }
    

    This annotation can be suitable for use on collection type fields or method collection parameters to identify objects in the collection. The field at the index position needs to be uniquely verified.

2. Define validation class

  • Create the validator class NotNullFieldValidator, implement the ConstraintValidator interface, and override the initialize and isValid methods.
    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;
    
        //Initialization method
        @Override
        public void initialize(UniqueProperty constraintAnnotation) {<!-- -->
            // Get the value of the annotation's index field
            index = constraintAnnotation.index();
        }
    
        // Verification logic
        @Override
        public boolean isValid(Collection<?> objects, ConstraintValidatorContext context) {<!-- -->
            if (CollectionUtils.isEmpty(objects)) {<!-- -->
                return false;
            }
    
            // Store the value of the field obtained at the index of each object in the collection
            Set<Object> uniqueValues = new HashSet<>();
            /*
             * Traverse the object array, that is, the object on which the annotation is applied
             * example:
             * @UniqueProperty(index = 2)
             * private List<User> list;
             * The parameter objects in the isValid method refers to the list
             * */
            for (Object obj : objects) {<!-- -->
                //Get the attribute value at the specified index
                Object param = getFieldValue(obj, index);
                if (param == null || uniqueValues.contains(param)) {<!-- -->
                    return false;
                }
                uniqueValues.add(param);
            }
            return true;
        }
    
        //Get the field value at the specified index
        public static Object getFieldValue(Object object, int index) {<!-- -->
            //Reflection to obtain the field (Field) collection of the object
            Field[] fields = object.getClass().getDeclaredFields();
            if (fields.length == 0) {<!-- -->
                throw new IllegalArgumentException("Object has no fields.");
            }
            try {<!-- -->
                // Get field name
                String fieldName = fields[index].getName();
                // Get the field object with the specified name fieldName
                Field field = object.getClass().getDeclaredField(fieldName);
                // Brute force cracking (including private fields)
                field.setAccessible(true);
                Object fieldValue = field.get(object);
                log.info("Attribute value uniqueness check: The value of {} is {}", fieldName, fieldValue);
                return fieldValue;
            } catch (NoSuchFieldException | IllegalAccessException e) {<!-- -->
                log.warn("Attribute value uniqueness verification exception: {}", e.getMessage());
                return null;
            }
        }
    }
    

3. Test our annotations

1. Note: Add the following dependencies in the pom.xml file

  • For the Integration and Autoconfiguration Java Bean Validation (JSR 380) framework.
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

2. Test-acts on method parameters

  • If javax.validation.Validator is not used to perform verification, verification will not be triggered automatically.

  • Here we can manually trigger the verification without calling it explicitly: use the @UniqueProperty annotation on the testUniqueAnnotation method parameter of MyService, And tell Spring Boot to perform verification through the @Validated annotation. If the fields of the objects in collection do not meet the conditions of @UniqueProperty, a ConstraintViolationException exception will be thrown.

    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 // Tell Spring Boot to perform verification through the @Validated annotation
    public class MyService {
    
        public Boolean testUniqueAnnotation(@UniqueProperty(index = 2, message = "idNumber is not unique") Collection<?> collection) {
            // Use @UniqueProperty annotation on method parameters
            // If the fields of the objects in the parameter collection do not meet the conditions of @UniqueProperty, a ConstraintViolationException will be thrown
            log.info("Collection content: {}", collection);
            return true;
        }
    }
    
  • Write Schoolfellow class

    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class Schoolfellow {<!-- -->
        private String name;
        private Integer age;
        private String idNumber;
    }
    
  • Write test unit and call the testUniqueAnnotation method in MyService.

    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);
        }
    
    }
    
  • Exception log of failed test

    Test exception picture.png

3. Test-acts on the fields of the class,

  • Write the School class and use the @UniqueProperty annotation on the List field of School.

    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;
    
    }
    
  • Add the testUniqueAnnotationByValid method in MyService, use the @Valid annotation to mark the class that needs to be verified, and then use this class in the method parameters (School ).

    public Boolean testUniqueAnnotationByValid(@Valid School school) {
        // Use @Valid annotation on method parameters
        log.info("Collection content: {}", school.getList());
        return true;
    }
    
    1. Write test unit and call the testUniqueAnnotationByValid method in MyService.
    @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);
    }
    
  • Exception log of failed test

    Side view 2.png

4. Special instructions

  • @Valid is a Java annotation, usually used to mark method parameters, method return values, fields, methods, constructors, etc. Its main function is to tell Bean Validation (defined in the JSR 380 specification Java Bean Validation Framework) should recursively validate objects marked @Valid when performing validation.

    Specifically, @Valid does the following:

    1. Use @Valid on method parameters: When using the @Valid annotation on method parameters, Bean Validation will automatically validate nested objects in the parameters recursively. This is useful for validating properties of complex objects to ensure properties in nested objects are also validated.
    public void createUser(@Valid User user) {<!-- -->
        //Verify the User object and its properties
    }
    
    1. Use @Valid on the method return value: You can also use the @Valid annotation on the method return value to ensure that the returned object is verified. This is useful for ensuring that the object returned by the service method is valid.
    @Valid
    public User getUser() {<!-- -->
        // Return User object
    }
    
    1. Using @Valid on fields: Although less common, it is also possible to use the @Valid annotation on fields of a class, usually in the case of nested objects. This will tell Bean Validation to validate the value of the field.
    public class Order {<!-- -->
        @Valid
        private ShippingAddress shippingAddress;
    
        // Getters and setters
    }
    

    In short, the @Valid annotation is mainly used to perform nested validation in the Bean Validation framework to ensure that validation recurses to objects marked @Valid property, and use it on the return value to ensure that the object returned by the method is validated. This helps ensure data integrity and consistency within the application.

In addition: You can also add some fields to the annotation and add verification rules, for example: add a Boolean canNull() within the annotation. code> is used to specify whether the value of the field can be null, and then obtain canNull in the verification class for judgment. Ensure exceptions and error conditions are handled appropriately in real-world applications to meet our real-world development needs.