Spring Boot uses JSR303 to implement parameter validation

The article begins with the official account of "Guo Guo Guo". Address: http://blog.itwolfed.com/blog/97

brief introduction

JSR-303 is a sub specification of JAVA EE 6, called Bean Validation.

At any time, when you want to deal with the business logic of an application, data validation is something you must consider and face. The application must use some means to ensure that the input data is semantically correct. In general, applications are layered, and different layers are completed by different developers. Many times, the same data validation logic will appear in different layers, which will lead to code redundancy and some management problems, such as semantic consistency. To avoid this, it is best to bind the validation logic to the corresponding domain model.

Bean Validation defines the corresponding metadata model and API for JavaBean validation. The default metadata is Java Annotations. By using XML, the original metadata information can be overwritten and extended. In an application, by using Bean Validation or your own defined constraint, for example @NotNull, @Max And @ ZipCode can ensure the correctness of the data model (JavaBean). Constraints can be attached to fields, getter methods, classes, or interfaces. For some specific requirements, users can easily develop customized constraint s. Bean Validation is a runtime data validation framework, after which the error information of validation will be returned immediately.

Constraint annotations embedded in the Bean Validation specification

example

Basic application

Introduce dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Add validation comment to parameter object

@Data
public class User {
    
    private Integer id;
    @NotBlank(message = "User name cannot be empty")
    private String username;
    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "Password must be 8~16 Combination of letters and numbers")
    private String password;
    @Email
    private String email;
    private Integer gender;

}

Add the parameter Bean to be verified in the Controller @Valid Turn on the verification function, and add a BindingResult immediately after the verified Bean. The BindingResult encapsulates the verification results of the previous Bean.

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("")
    public Result save (@Valid User user , BindingResult bindingResult)  {
        if (bindingResult.hasErrors()) {
            Map<String , String> map = new HashMap<>();
            bindingResult.getFieldErrors().forEach( (item) -> {
                String message = item.getDefaultMessage();
                String field = item.getField();
                map.put( field , message );
            } );
            return Result.build( 400 , "illegal parameter !" , map);
        }
        return Result.ok();
    }

}

The tests are as follows:

Unified handling of exceptions

When the parameter verification fails, a bindexception exception exception will be thrown, which can be processed uniformly in the unified exception processing, so that the BindingResult will not be used to get the verification result in every place where the parameter verification is required.

@Slf4j
@RestControllerAdvice(basePackages = "com.itwolfed.controller")
public class GlobalExceptionControllerAdvice {

    @ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
    public Result handleVaildException(Exception e){
        BindingResult bindingResult = null;
        if (e instanceof MethodArgumentNotValidException) {
            bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
        } else if (e instanceof BindException) {
            bindingResult = ((BindException)e).getBindingResult();
        }
        Map<String,String> errorMap = new HashMap<>(16);
        bindingResult.getFieldErrors().forEach((fieldError)->
                errorMap.put(fieldError.getField(),fieldError.getDefaultMessage())
        );
        return Result.build(400 , "illegal parameter !" , errorMap);
    }

}

Group solution verification

The verification rules for entities are different between adding and modifying. For example, when the id is auto increasing, the id must be empty when adding, and it must not be empty when modifying. When adding and modifying, if the same entity is used, group verification is needed.

The verification annotation has a groups attribute. You can group the verification annotation. Let's look at the source code of @ NotNull:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {

	String message() default "{javax.validation.constraints.NotNull.message}";

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

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

	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	@interface List {

		NotNull[] value();
	}
}

From the source code, we can see that Groups is an array of class <? > type, so we can create a group

public class Groups {
    public interface Add{}
    public interface  Update{}
}

Adding groups to validation notes for parameter objects

@Data
public class User {

    @Null(message = "New does not need to be specified id" , groups = Groups.Add.class)
    @NotNull(message = "Modification needs to be specified id" , groups = Groups.Update.class)
    private Integer id;
    @NotBlank(message = "User name cannot be empty")
    @NotNull
    private String username;
    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "Password must be 8~16 Combination of letters and numbers")
    private String password;
    @Email
    private String email;
    private Integer gender;

}

The original @ Valid of the Controller cannot specify a group. It needs to be replaced with @ Validated

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("")
    public Result save (@Validated(Groups.Add.class) User user)  {
        return Result.ok();
    }

}

The tests are as follows:

Custom verification notes

Although JSR303 and springboot validator have provided a lot of validation annotations, they still can't meet our requirements when facing complex parameter validation. In this case, we need to customize validation annotations.

For example, gender in User uses 1 for male and 2 for female. We define a verification annotation @ ListValue, which can only be 1 and 2.

Create constraint rules

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "";

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

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

    int[] vals() default { };
}

An annotation is defined by @ interface keyword. The attributes in this annotation are declared as similar methods According to the requirements of Bean Validation API specification:

  • Message attribute, which is used to define the default message template. When the constraint fails to be verified, the This property to output error information.
  • The groups attribute is used to specify which validation groups this constraint belongs to. The default value of this must be an array of type class <? >.
  • The payload property, which allows users of the Bean Validation API to specify severity levels for constraints. This property is not used by the API itself.

In addition to the three mandatory attributes (message, groups and payload), we also add A property is added to specify the required value. The name vals of this property is more specific in the definition of annotation However, if only this attribute is assigned, the attribute name can be ignored when using this annotation

In addition, we also annotate the annotation with some meta annotations annotatioins):

  • @Target({ METHOD, FIELD, ANNOTATION_TYPE}): indicates that this annotation can be used in methods, fields, or On the annotation declaration.
  • @Retention(RUNTIME): indicates that the annotation information is read by reflection during runtime
  • @Constraint(validatedBy = ListValueConstraintValidator.class ): indicates which validator (class) is used to verify the element with this annotation
  • @Documented: indicates that the annotation will be added to the javadoc

Create constraint validator

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();
    /**
     * Initialization method
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {

        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }

    }

    /**
     * Judge whether the verification is successful
     *
     * @param value Value to be verified
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {

        return set.contains(value);
    }
}

ListValueConstraintValidator defines two generic parameters. The first is the type of annotation (listvalue in our case) served by this validator, and the second is the type of element (Integer) supported by this validator.

If a constraint dimension supports multiple types of verified elements, you need to define a constraint validator for each supported type and register it in the constraint dimension.

The implementation of this verifier is very common. The initialize() method passes in an instance of the annotation type to be verified. In this In the example, we use this instance to get the value of its vals attribute and save it as a Set set for the next step Use.

isValid() is the place to implement the real verification logic. To judge a given int for the @ ListValue constraint Is it legal.

Use the @ ListValue annotation in the parameter object.

@Data
public class User {

    @Null(message = "New does not need to be specified id" , groups = Groups.Add.class)
    @NotNull(message = "Modification needs to be specified id" , groups = Groups.Update.class)
    private Integer id;
    @NotBlank(message = "User name cannot be empty")
    @NotNull
    private String username;
    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "Password must be 8~16 Combination of letters and numbers")
    private String password;
    @Email
    private String email;
    @ListValue( message = "Gender should be assigned a value" , vals = {1,2} , groups = {Groups.Add.class , Groups.Update.class})
    private Integer gender;

}

The tests are as follows:

Source address

https://github.com/gf-huanchupk/SpringBootLearning

reference resources

https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html https://docs.jboss.org/hibernate/validator/4.3/reference/zh-CN/pdf/hibernate_validator_reference.pdf

Pay attention to me

Welcome to scan code or WeChat search official account "programmer Guo Guo" pay attention to me, and have a surprise.

Tags: Programming Bean Validation Attribute Java xml

Posted on Mon, 18 May 2020 23:42:45 -0400 by TobesC