[Java] Parameter verification and unified exception handling

Java parameter verification and unified exception handling

if/else[Preface] Parameter verification is an indispensable part of interface development. In the past, the verification parameters were basically realized by a large number of control statements. Later 反射+自定义注解, the form can be used for verification, but the reusability is not very good. Among them, the description of the protocol on parameter verification in the "Ali Development Manual":
insert image description here

How to elegantly verify the legality of interface parameters? Spring has developed a validated framework for annotation verification, which can save a lot of redundant verification implementation logic details, and improve performance for development and code maintenance.

PART1. Basic concepts


  • JSR-303
    JSR is the abbreviation of Java Specification Requests (Java specification proposal). Anyone can submit a JSR to add new APIs and services to the Java platform. JSR has become an important standard in the Java world.
    JSR-303 is a sub-specification in JAVA EE 6, called Bean Validation , which is a set of annotation-based data validation specifications defined by Java.

  • Hibernate Validator

    Hibernate Validator is the reference implementation of Bean Validation. Hibernate Validator provides the implementation of all built-in constraints in the JSR 303 specification, in addition to some additional constraints.

    Its pom reference is:

<!--jsr 303-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.1</version>
</dependency>
<!-- hibernate validator-->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>

Their relationship is that Hibernate Validator is the implementation of Bean Validation. In addition to the JSR specification, it also adds some of its own constraint implementations, so click on the pom and find that Hibernate Validator relies on validation-api.
Because Java EE was renamed Jakarta EE in 2018, jakarta.validation is renamed from javax.validation.
For spring boot applications, you can directly refer to the starter it provides

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

Spring boot has its version number configuration, which inherits the pom of spring boot, so there is no need to specify the version number yourself.
This starter also relies on Hibernate Validator internally. Hibernate Validator internally relies on jakarta.validation-api.

PART2.Validator parameter verification


Implementation and usage: Generally, two annotations are used more often: @Validated@Valid

The difference between @valid and @validate

the difference @Valid @Validated
provider JSR-303 specification Spring
Whether to support grouping not support support
label position METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
nested validation support not support

ElementType.TYPE: can modify classes, interfaces or enumeration types
ElementType.FIELD: can modify member variables
ElementType.METHOD: can modify methods
ElementType.PARAMETER: can modify parameters
ElementType.CONSTRUCTOR: can modify constructors
ElementType.LOCAL_VARIABLE: can modify local Variable
ElementType.ANNOTATION_TYPE: can modify annotation
ElementType.PACKAGE: can modify package

TYPE_USE can be used to mark any type (excluding class)

Commonly used annotations for specific verification functions are as follows:

Constraint illustrate Supported data types
@AssertFalse Annotated elements must be false Boolean
@AssertTrue Annotated elements must be true Boolean
@DecimalMax The annotated element must be a number whose value must be less than or equal to the specified maximum value BigDecimal, BigInteger, CharSequence, byte, short, int, long
@DecimalMin The annotated element must be a number whose value must be greater than or equal to the specified minimum BigDecimal, BigInteger, CharSequence, byte, short, int, long
@Max The annotated element must be a number whose value must be less than or equal to the specified maximum value BigDecimal, BigInteger, byte, short, int, long
@Min The annotated element must be a number whose value must be greater than or equal to the specified minimum BigDecimal, BigInteger, byte, short, int, long
@Digits(integer=,fraction=) Checks if the value of the annotation is a number with at most integer and fraction BigDecimal, BigInteger, CharSequence, byte, short, int, long
@Email The commented element must be an email address, the optional parameters regexp and flag allow specifying additional regular expressions (including regular expression flags) that must match CharSequence
@Future The annotated element must be a future date Date,Calendar,Instant,LocalDate等
@FutureOrPresent The annotated element must be a future date or the current date Date,Calendar,Instant,LocalDate等
@Past The annotated element must be a past date Date,Calendar,Instant,LocalDate等
@PastOrPresent The annotated element must be a past date or a current date Date,Calendar,Instant,LocalDate等
@NotBlank The commented element is not null, and the length is greater than 0 after removing the blank characters on both sides CharSequence
@NotEmpty the annotated element is not null and the collection is not empty CharSequence, Collection, Map, arrays
@NotNull the annotated element is not null Any type
@Null The annotated element is null Any type
@Pattern(regex=, flags=) The commented element must match the regular expression regex CharSequence
@Size(min=, max=) Annotated element size must be between min and max (closed range) CharSequence, Collection, Map,arrays

Next, a simple demo is performed on the practice of parameter verification:

  1. Define a parameter entity class, and add corresponding annotations to the parameters that need to be verified
@Data
public class UserInfo {
    
    

    @NotBlank(message = "姓名不能为空")
    public String name;

    @Min(value = 10, message = "年龄不得少于10岁")
    public int age;
    
}

2. Define a Controller class to perform parameter verification on the @Validated annotation on the entry

@RestController
@Slf4j
public class TestController {
    
    

    @PostMapping("getInfo")
    public String test(@RequestBody @Validated UserInfo userInfo) {
    
    
        log.info("入参:【{}】", userInfo);
        return "success";
    }
}

The result of the request is as follows:
insert image description here

PART3. Group verification


Sometimes our multiple interfaces will reuse a parameter object, and the parameters in it are required in interface A, but not required in interface B.

Scenarios like this require the use of group verification methods, and the flexible collocation of group verification is sufficient for most scenarios.

There is a concept of groups in Validator, which can help us specify different verification groups through this parameter.

ValidatedIt has its own default group Default.class. When we need to add a new group verification based on the default verification, it is recommended to inherit Default, because the default groupsis groups = {Default.class}.

insert image description here

Step1. Set up the grouping interface

The group we want to build is a group divided into fields used by different businesses. The example business is a user object, and users have different roles. Different interfaces will use different fields of this user object. For example, student ( Student), teacher ( Teacher):

public interface Student {
    
    
}
public interface Teacher {
    
    
}

Step2. Add groups to the parameters that require group verification

@Data
public class UserInfo {
    
    

    @NotBlank(message = "姓名不能为空")
    public String name;

    @Min(value = 10, message = "年龄不得少于10岁", groups = {
    
    Default.class})
    public int age;

    @NotBlank(message = "手机号不能为空", groups = {
    
    Teacher.class})
    public String phone;

    @NotEmpty(message = "授课科目不能为空", groups = {
    
    Teacher.class})
    @Size(min = 2, message = "必须至少两个科目", groups = {
    
    Teacher.class})
    private List<String> subjects;

}

Step3. Add the corresponding group on @Validated

@RestController
@Slf4j
public class TestController {
    
    

    @PostMapping("getInfo")
    public String test(@RequestBody @Validated({
    
    Teacher.class}) UserInfo userInfo) {
    
    
        log.info("入参:【{}】", userInfo);
        return "success";
    }
}

ps. Group inheritance verification:

Custom groups can be verified using inheritance. For example, we encapsulate many groups into a specific group, which is convenient for us to combine freely. Please see the following cases for multiple custom groups:

public interface GroupsOpration extends GroupUpdate{
    
    
}
public interface GroupUpdate extends Default {
    
    
}
public interface GroupDel extends Default {
    
    
}
public interface GroupAdd extends Default {
    
    
}

PART4. Object validation


Sometimes the input parameter of our interface contains a collection object element, and each attribute of this object also needs to be verified separately. For this scenario, we can combine the @Valid annotation to perform nested verification. The example is as follows:

Step1. Redefine an input parameter UsersVo, which contains the collection attribute userInfoList marked by @Valid annotation, and the validation constraints of each element UserInfo object refer to the previous definition:

@Data
public class UsersVo {
    
    

    @NotBlank(message = "ID不能为空")
    public String id;

    @Valid
    @NotEmpty
    public List<UserInfo> userInfoList;

}

Step2.Controller layer interface parameters add @Validated annotation to achieve the purpose of nested validation

 @PostMapping("getInfos")
 public String test(@RequestBody @Validated UsersVo usersVo) {
    
    
    log.info("入参:【{}】", usersVo);
    return "success";
}

PART5. Custom Validator


If the default annotation rules cannot meet the business needs, validatorthe form of custom annotations is provided to help developers perform custom rule verification.

Step1. Define custom annotations :

The first step is to determine the annotations that you need to customize. For example, I have defined an annotation to check whether the name starts with Zake.

@Target({
    
    ElementType.FIELD}) // 可以注入的类型,字段和参数类型
@Retention(RUNTIME) // 运行时生效
@Constraint(validatedBy = {
    
    CheckNameValidator.class}) // 指定用于验证元素的验证器
public @interface CheckName {
    
    

    String message() default "姓名不正确";   //提示的信息

    Class<?>[] groups() default {
    
     };  //分组验证,例如只在新增时进行校验等

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

}

The three attributes message, groups, mustpayload be defined, otherwise, the following error will be thrown during verification

Step2. Define the real annotation processing class :

The interface needs to be implemented ConstraintValidator. The first parameter of the generic type is the annotation class , and the second parameter is the type of the specific verification object

public class CheckNameValidator implements ConstraintValidator<CheckName, String> {
    
    

    // 初始化注解的校验内容
    @Override
    public void initialize(CheckName constraintAnnotation) {
    
    
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    // 具体的校验逻辑
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
    
    
        return value.startsWith("zake");
    }
}

Step3, add annotations to parameter variables

@Data
public class UserInfo {
    
    

    @NotBlank(message = "姓名不能为空")
    @CheckName
    public String name;

    @Min(value = 10, message = "年龄不得少于10岁", groups = {
    
    Default.class})
    public int age;

    @NotBlank(message = "手机号不能为空", groups = {
    
    Teacher.class})
    public String phone;

    @NotEmpty(message = "授课科目不能为空", groups = {
    
    Teacher.class})
    @Size(min = 2, message = "必须至少两个科目", groups = {
    
    Teacher.class})
    private List<String> subjects;

}

PART6. Exception handling


The above achieves the goal of parameter verification, but the prompt of exception information is not friendly, and the exception handling can be further optimized.

How to handle validate exception information?

  • BindingResult information processing
  • Controller for specific exception handling
  • Unified exception handling
1. BindingResult information processing

validate provides BindResultobject encapsulation exception information, you need to follow the object immediately@Validated after the annotation parameter position, be careful to follow it, otherwise it cannot be injected, after adding, after the object fails to be validated, the BindResultbasic exception information encapsulated in the object can be Freely handled by the developer.

  @PostMapping("getInfo")
    public String test(@RequestBody @Validated({
    
    Teacher.class}) UserInfo userInfo, BindingResult result) {
    
    
        log.info("入参:【{}】", userInfo);
        if (result.hasErrors()) {
    
    
            FieldError fieldError = result.getFieldError();
            String field = fieldError.getField();
            String msg = fieldError.getDefaultMessage();

            return field + ":" + msg;   // 异常信息的处理返回
        }
        return "success";
    }
2. The controller performs specific exception handling

Generally, this method is rarely used. In the case of global exception handling, exception handling is rarely performed at the Controller layer, and it can be used in some special cases.

This method is similar to the global exception handler, except that the definition method is modified to the corresponding Controllercontroller layer.

@RestController
@Slf4j
public class TestController {
    
    

    @PostMapping("getInfo")
    public String test(@RequestBody @Validated({
    
    Teacher.class}) UserInfo userInfo) {
    
    
        log.info("入参:【{}】", userInfo);
        return "success";
    }

    /**
     在控制器层处理异常信息,仅仅适用于当前控制器
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object processException(MethodArgumentNotValidException e){
    
    
        log.error(e.getMessage());
        return e.getAllErrors().get(0).getDefaultMessage();
    }

}
3. Unified exception handling (recommended)

Global unified exception handling is the most commonly used processing method. This method assembles exception information into custom results, and can also be used for logging and processing. It is handled as follows:

Step1. Create a new global unified exception handling class, and 类名mark: @ControllerAdviceor @RestControllerAdvice, corresponding to the Controller layer annotation @Controllerand respectively @RestController.

@ControllerAdvice -> @Controller
@RestControllerAdvice -> @RestController

Step2. Inside the corresponding method in the global unified exception handling class, use @ExceptionHandlermethod annotation. You can add parameters to the @ExceptionHandler annotation. The parameter is the class of a certain exception class, which means that this method specifically handles this type of exception. When an exception occurs, Spring will choose the processing method closest to throwing an exception.

The code example is as follows:

Define error code enumeration

@Getter
public enum ResultCodeEnum {
    
    

    SUCCESS(200, "成功"),
    FAILED(1001,"失败"),
    ERROR(500, "系统内部错误");


    private int code;
    private String message;

    ResultCodeEnum(int code, String message) {
    
    
        this.code = code;
        this.message = message;
    }
}

Define a unified response format:

@Data
public class ResultInfo<T> {
    
    

    public int code;

    public String message;

    public T data;

    public ResultInfo(T data){
    
    
        this.code = ResultCodeEnum.SUCCESS.getCode();
        this.message = ResultCodeEnum.SUCCESS.getMessage();
        this.data = data;
    }

    public ResultInfo(int code, String msg) {
    
    
        this.code = code;
        this.message = msg;
    }
}

The global exception handler increases the interception of exception information and modifies the data format returned when an exception occurs.

@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
    
    

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultInfo<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
    
    
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        // 然后提取错误提示信息进行返回
        return new ResultInfo<>(ResultCodeEnum.FAILED.getCode(), objectError.getDefaultMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResultInfo<String> ExceptionHandler(Exception e) {
    
    
        return new ResultInfo<>(ResultCodeEnum.ERROR.getCode(), ResultCodeEnum.ERROR.getMessage());
    }

}

Guess you like

Origin blog.csdn.net/zhzh980/article/details/129229479