Java Series -- JSR-303 Form Validation

The reason is that a web project was created using springboot, and an interface for creating a new user was written with the help of mybatis. At this time, the parameters passed to the client, how to verify, which field must be passed, and how to return a field if a field is empty The specified string message? In this way, I found JSR303 the specification

1.0 What is JSR-303?

1.1 JSR303 official document

JSR-303 is a set of Java API specifications for validating JavaBean , also known as JSR-303 bean validation 1.0. Of course, as of now, the bean validation specification has been updated to 1.1 (JSR-349), 2.0 (JSR380), 3.0 and other versions in the past two decades.

Fortunately, there is no need to worry that the content of this article is outdated, because the upgraded version only adds some usable annotations, and the core content remains unchanged. (There is a link at the end of the article about the historical version)

1.2 Implement the JSR-303 specification

JSR-303 is a specification, so how should I implement it? 第一个例子 2.1.2As described in the official document : 1. First create an annotation that conforms to the specification, 2. Then mark the annotation class on the entity class

First, create an annotationOrderNumber

  1. An annotation class that complies with the JSR-303 standard must have 3 fields
  2. 字段 messageDefinition: The error message returned. Used to return the content when the JavaBean validation fails
  3. 字段 groupsDefinition: This element specifies the processing group associated with the constraint declaration. For example, if we set our nickname, we may set multiple restrictions, only English is allowed, and the length is within 10. Start with yyds_... Deng, etc. Rules with multiple restrictions like this can be defined in a group NamesGroups. Of course, the default bit is empty, and the rule is separate.
  4. 方法 payload()Definition: A payload element that specifies the payload associated with the constraint declaration. This method usually specifies which fields the annotation can only be added to. It is empty by default and can be added to any field
package com.acme.constraint;

/**
 * Mark a String as representing a well formed order number
 */
@Documented
@Constraint(validatedBy = OrderNumberValidator.class)
@Target({
    
     METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface OrderNumber {
    
    
    String message() default "{com.acme.constraint.OrderNumber.message}";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };
}

After creating it, use it on the field, it's that simple

public class UserEntity implements Serializable {
    
    
    @OrderNumber
    private String oneOrder;
}

1.3 The purpose of designing JSR303 and the desired effect

At present, it seems that this specification is very simple, it only defines a few fields and methods, it seems nothing special, but for a specification, there are actually many things to consider, some official descriptions are extracted (interested You can check the official documentation):

Validating data is a common task that occurs throughout an application, from the presentation layer to the persistence layer. Often, the same validation logic is implemented in each layer, which proves to be time-consuming and error-prone. To avoid duplicating these validations in each layer, developers often bundle validation logic directly into the domain model, intermingling domain classes with validation code (actually metadata about the classes themselves).

This JSR defines the metadata model and API for JavaBean validation. The default source of metadata is annotations, which can be overridden and extended by using XML validation descriptors.

The validation API developed by this JSR does not apply to any one layer or programming model. It specifically does not depend on the Web layer or the persistence layer, and can be used for server-side application programming as well as rich-client Swing application developers. This API is considered a general extension of the JavaBeans object model and is therefore expected to be used as a core component in other specifications. Ease of use and flexibility influenced the design of this specification

2.0 Validation Framework

2.1 List the frameworks that implement JSR-303the specification.

We've covered JSR-303what a specification is and how to implement it.

In the actual development work, it would be very troublesome to implement it by ourselves, and the subsequent upgrade and handover would also be a headache. If there is a ready-made one, it would be great to implement this standardized framework.

So I found several general frameworks, just need to bring in the coordinates.

The following are the two most common and common scenarios:

2.2.1 If it is a web application created through spring boot, andspring boot (version<2.3)

The JSR-303 specification has been implemented by default within the framework, and its ready-made annotations can be used directly

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

2.2.2 If it is a web application created through spring boot, andspring boot (version>=2.3)

Since the implementation has been stripped inside the web framework, this dependency package needs to be introduced:

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

2.2.3 If it is a non-Spring boot application, the following dependencies need to be introduced

<dependencies>
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>${api-version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>${hibernate-validator-version}</version>
    </dependency>
</dependencies>

2.4 If you are using gradle to compile

compile group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.2.6.RELEASE'

What are the embedded annotations in 3.0?

Bean Validation 的内嵌的注解The following also means that the imported framework conforming to the JSR303 specification also implements these annotations internally.

annotation details
@Null The annotated element must be null
@NotNull The annotated element must not be null
@@AssertTrue Annotated elements must be true
@@AssertFalse Annotated elements must be false
@@Min(value) The annotated element must be a number whose value must be greater than or equal to the specified minimum
@Max(value) The annotated element must be a number whose value must be less than or equal to the specified maximum value
@DecimalMin(value) The annotated element must be a number whose value must be greater than or equal to the specified minimum
@DecimalMax(value) The annotated element must be a number whose value must be less than or equal to the specified maximum value
@Size(max, min) The size of the annotated element must be within the specified range
@Digits (integer, fraction) The annotated element must be a number whose value must be within the acceptable range
@Past The annotated element must be a past date
@Future The annotated element must be a future date
@Pattern(value) Annotated elements must conform to the specified regular expression

Example usage:

// 在字段上使用
public class UserEntity implements Serializable {
    
    

    @Null
    private Integer id;

    @NotNull
    private String userName;

    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,120}$", message = "密码最少为8位字母和数字组合")
    private String pwd;

    @NotBlank(message = "性别不能为空")
    @Min(value = 0,message = "性别不能既非男又非女")
    private String sex;

    @Max(value = 100,message = "不准超过65")
    private Integer age;

    @DecimalMin(value="200000", message = "工资不能低于每月2W")
    private Integer salary;

    @Past
    private Date pastDay;
}
// 在方法上使用
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Validated
public interface SystemUserService {
    
    
   // 注销用户
    void logOff(@Min(1) long userId,@NotNull @Size(min = 10, max = 200) String reasonForlogOff);
}

Tips: If you want to see how many internal annotations there are, there are only a dozen in total, then you can jump into the source code through annotations in the project to view
insert image description here

4.0 Verify request content

Usually, we are controllervalidating the request parameters here, just need to add @Valid, very simple:

import javax.validation.Valid;
import org.springframework.validation.BindingResult;
...

@RestController
public class UserController {
    
    

    @PostMapping("/users")
    ResponseEntity<String> addUser(@Valid @RequestBody User user, BindingResult 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 ResponseEntity.error(map.toString());
        }
        return ResponseEntity.ok("创建成功");
    }
}

4.1 Group verification

Assume such a situation: when we create a new user, we don’t need to pass the user ID, let the database auto-increment the ID, but when modifying the user, we need to pass a user ID, and the same entity receives parameters for adding and modifying. How should the constraints be written?

  1. Create a class dedicated to distinguishing whether it is new or updated
public class Groups {
    
    
    public interface Add{
    
    }
    public interface  Update{
    
    }
}
  1. Added on the field label groups. When added, the constraint @Nulltakes effect. When updating, constraints @NotNullare in effect.
// 在字段上使用
public class UserEntity implements Serializable {
    
    

    @Null(message = "新增不需要指定id" , groups = Groups.Add.class)
    @NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
    private Integer id;

    @NotNull
    private String userName;
}
  1. In the controller, @Validit will be changed to @Validated(Groups.Add.class). SR303's own @Valid does not support group verification, but Spring provides an annotation @Validated to support group verification
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
...

@RestController
public class UserController {
    
    

    @PostMapping("/users")
    ResponseEntity<String> addUser(@Validated(Groups.Add.class) @RequestBody 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 ResponseEntity.error(map.toString());
        }
        return ResponseEntity.ok("创建成功");
    }
}

At this time, when the request is initiated, the parameter id is passed, and an error message will be returned:新增不需要指定id

### 新增用户
POST http://localhost:9000/user/register
Content-Type: application/json

{
    
    
  "id": 1234,
  "phone": 13366668888,
  "pwd": "12345678abc"
}

4.2 Nested Validation

@ValidWhat is nested validation? To put it bluntly, it is an entity, and one of the fields is another entity. Then just add a comment to this field.

public class UserEntity implements Serializable {
    
    

    @Null(message = "新增不需要指定id" , groups = Groups.Add.class)
    @NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
    private Integer id;

    /**
     * @Valid 重要
     */
    @Valid
    @NotNull
    private RoleEntity role;
}
class RoleEntity implements Serializable {
    
    
    @NotBlank(message = "角色名称不能为空")
    private String roleName;
}

@ValidatedIt should be noted that the nested verification has strict verification for the group, that is to say, the group should not be specified when used in the controller

import javax.validation.Valid;
import org.springframework.validation.BindingResult;
...

@RestController
public class UserController {
    
    

    // @Validated 不能指定分组为 groups = Groups.Add.class,否则嵌套校验role会失效
    @PostMapping("/users")
    ResponseEntity<String> addUser(@Validated @RequestBody 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 ResponseEntity.error(map.toString());
        }
        return ResponseEntity.ok("创建成功");
    }
}

4.3 Custom Validator

Although the built-in constraint annotations are sufficient for daily development, sometimes they cannot meet the requirements and some validation constraints need to be customized.

For example: In this example, only 1 and 2 are allowed in the gender field, otherwise the verification fails.

Three steps:

  1. Create annotations (receive constraint data)
  2. Constraint classes implementing custom rules (defining constraint rules)
  3. call test (pass in constraint data)

1. Create annotations DefineRangeValidationValues, where additional rules specified on the class @NotNullwill also take effect. In addition, for general purpose, create a values ​​​​field to receive later incoming data

@Documented
@Constraint(validatedBy = DefineRangeValidator.class)
@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@NotNull(message = "不能为空")
public @interface DefineRangeValidationValues {
    
    
    String message() default "传入的值不在范围内";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };

    /**
     * @return 传入的值
     */
    int[] values() default {
    
    };
}
  1. Implement custom constraint rule classesDefineRangeValidator
package com.mock.water.core.validation;

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

/**
 * $$ 第一个泛型是校验注解,第二个是参数类型
 *
 * @author [email protected]
 * @version 1.0.0
 * @date 2022/10/14 17:18
 **/
public class DefineRangeValidator implements ConstraintValidator<DefineRangeValidationValues, Integer> {
    
    
    /**
     * 存储枚举值
     */
    private Set<Integer> enumValues = new HashSet<>();

    /**
     * 初始化
     *
     * @param constraintAnnotation 约束注释
     */
    @Override
    public void initialize(DefineRangeValidationValues constraintAnnotation) {
    
    
        ConstraintValidator.super.initialize(constraintAnnotation);
        for (int value : constraintAnnotation.values()) {
    
    
            enumValues.add(value);
        }
    }
    /**
     * 是否有效
     *
     * @param value                      整数
     * @param constraintValidatorContext 约束验证器上下文
     * @return boolean
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
    
    
        //判断是否包含这个值
        return enumValues.contains(value);
    }
}
  1. have a test
// 在字段上使用
public class UserEntity implements Serializable {
    
    
    @DefineRangeValidationValues(values = {
    
    1, 2}, message = "性别只能传入1或者2")
    private Integer sex;
}

5.0 Receive verification results

In the above example, we have seen that we can BindingResultaccept parameters through the classes provided by the validation framework.

The disadvantage of this method is obvious. Each interface needs to write a statement at the position where the parameter is passed, and process the verification result. Therefore the correct course of action is to全局异常捕捉

6.0 Global Exception Capture

When the parameters fail to be verified , one MethodArgumentNotValidExceptionor BindExceptiontwo exceptions will be thrown. These two exceptions can be caught in the global exception handler, and the prompt information or custom information will be returned to the client.

The author will not post other exception captures in detail here, just post the exception capture of parameter verification

package com.mock.water.core.group.exception;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * $$
 *
 * @author [email protected]
 * @version 1.0.0
 * @date 2022/10/14 16:45
 **/
@RestControllerAdvice
public class GlobalExceptionHandler {
    @Autowired
    private ObjectMapper objectMapper;
    /**
     * 参数校验异常步骤
     */
    @ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
    public String onException(Exception e) throws JsonProcessingException {
        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 objectMapper.writeValueAsString(errorMap);
    }
}

Extended reading


------ If the article is useful to you, thank you in the upper right corner >>> Like | Favorite<<<

Guess you like

Origin blog.csdn.net/win7583362/article/details/127324059