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 oncollection type fields
ormethod collection parameters
toidentify objects in the collection. The field at the index position
needs to beuniquely verified
.
2. Define validation class
- Create the validator class
NotNullFieldValidator
, implement theConstraintValidator
interface, and override theinitialize
andisValid
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 thetestUniqueAnnotation
method parameter ofMyService
, And tell Spring Boot to perform verification through the@Validated
annotation. If the fields of the objects incollection
do not meet the conditions of@UniqueProperty
, aConstraintViolationException
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 thetestUniqueAnnotation
method inMyService
.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
3. Test-acts on the fields of the class,
-
Write the
School class
and use the@UniqueProperty
annotation on theList
field ofSchool
.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 inMyService
, 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; }
- Write
test unit
and call thetestUniqueAnnotationByValid
method inMyService
.
@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); }
- Write
-
Exception log of failed test
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:- 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 }
- 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 }
- 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. - Use
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.