1, the preface

In the business system, parameter verification is a headache. Some entity classes are as long as dozens of fields, and the large if-else will not only make the children who write the code headache, but also the people who receive the project later, when they see these codes, it is estimated to be more headache.

So how do you avoid redundant code?

In Spring Boot we can use Validation to validate parameters;

<! -- Parameter verification -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Copy the code

The restrictive notes are as follows:

annotations function
@AssertFalse Can be null, but must be false if not
@AssertTrue May be null, but must be true if not
@DecimalMax The value cannot exceed the maximum value
@DecimalMin The setting cannot exceed the minimum value
@Digits The setting must be numeric and the number of digits of the numeric integer and the number of digits of the decimal must be within the specified range
@Future Date must be in the future of the current date
@Past The date must be past the current date
@Max The maximum value must not exceed this value
@Min The maximum must not be less than the minimum
@NotNull It cannot be null, but can be null
@Null Must be null
@Pattern The specified regular expression must be satisfied
@Size Size () values for collections, arrays, maps, and so on must be within the specified range
@Email The value must be in email format
@Length The length must be within the specified range
@NotBlank The string must not be null, and the string trim() must not equal “.
@NotEmpty The value cannot be null, and the size() of collections, arrays, and maps cannot be 0. The string trim() can be equal to “”
@Range Values must be within the specified range
@URL It must be a URL

2. Code implementation

2.1 valiation verification

Student entity class

package com.scaffold.test.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

/ * * *@author alex wong
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Student implements Serializable {

    private static final long serialVersionUID=1L;

    @range (min = 1, message = "id cannot be empty ")
    private int id;

    @notblank (message = "name cannot be empty ")
    private String name;

    @notnull (message = "age cannot be empty ")
    private Integer age;

}
Copy the code

com.scaffold.test.controller.StudentController

package com.scaffold.test.controller;

import com.scaffold.test.entity.Student;
import com.scaffold.test.service.StudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/** ** <p> * Front-end controller * </p> **@author alex wong
 */

@Slf4j
@RestController
@RequestMapping("/student")
public class StudentController {
    
    @GetMapping("add")
    public String addStudent(@Validated Student student, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            if(bindingResult.hasErrors()){
                for (ObjectError error: bindingResult.getAllErrors()) {
                    log.error(error.getDefaultMessage());
                    returnerror.getDefaultMessage(); }}}return "add"; }}Copy the code

Postman visit http://192.168.66.65:9002/student/add

Postman visit http://192.168.66.65:9002/student/add? name=wz

Postman visit http://192.168.66.65:9002/student/add? name=wz&id=3

All error alerts are implemented with bindingResult:

if(bindingResult.hasErrors()){
    if(bindingResult.hasErrors()){
        for (ObjectError error: bindingResult.getAllErrors()) {
            log.error(error.getDefaultMessage());
            returnerror.getDefaultMessage(); }}}Copy the code

The BindingResult class is used to hold the exception information. When the verification fails, we only need to process the exception information in BindingResult.

If this code was added to every interface code, it would still seem like a hassle, but what would you do?

Can you do global error capture?

2.2. Add global exception handling

We modify the code to add @Validated:

@RestController
@RequestMapping("/student")
public class StudentController {    
    
	@GetMapping("add")
    public String addStudent(@Validated Student student){
        returnstudentService.saveStudent(student); }}Copy the code

Add global exception handling

com.scaffold.test.config.WebMvcConfig

package com.scaffold.test.config;

import com.alibaba.fastjson.JSON;
import com.scaffold.test.base.Result;
import com.scaffold.test.base.ResultCode;
import com.scaffold.test.base.ServiceException;
import com.scaffold.test.config.interceptor.AuthenticationInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/ * * *@author alex
 */

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final Logger logger = LoggerFactory.getLogger(WebMvcConfigurer.class);

    /** * unified exception handling *@param exceptionResolvers
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        exceptionResolvers.add((request, response, handler, e) -> {
            Result result = new Result();
            // Exception handling
            if (e instanceof ServiceException) {
                // 1. Service failure exceptions, such as incorrect account or password
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
                logger.info(e.getMessage());
            }else if (e instanceof ServletException) {
                // 2. Call failed
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
            } else {
                // 3. Other internal errors
                result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage("Interface [" + request.getRequestURI() + "] Internal error, please contact administrator");
                String message;
                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = (HandlerMethod) handler;
                    message = String.format("Interface [%s] exception, method: %s.%s, exception Summary: %s",
                            request.getRequestURI(),
                            handlerMethod.getBean().getClass().getName(),
                            handlerMethod.getMethod().getName(),
                            e.getMessage());
                } else {
                    message = e.getMessage();
                }
                result.setMessage(message);
                logger.error(message, e);
            }
            responseResult(response, result);
            return new ModelAndView();
        });
    }

    // Process the response data format
    private void responseResult(HttpServletResponse response, Result result) {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type"."application/json; charset=UTF-8");
        response.setStatus(200);
        try {
            response.getWriter().write(JSON.toJSONString(result));
        } catch(IOException ex) { logger.error(ex.getMessage()); }}}Copy the code

However, such error messages still look messy.

org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'student' on field 'id': rejected value [0]; codes [Range.student.id,Range.id,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.id,id]; arguments []; default message [id],9223372036854775807.1]; defaultMessage [ID cannot be empty]\nField error in object'student' on field 'age': rejected value [null]; codes [NotNull.student.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.age,age]; arguments []; default message [age]]; defaultMessage [age cannot be empty]Copy the code

The ideal message would be:

{
    "code": 500."message": "Id can't be empty, age can't be empty."
}
Copy the code

So how to deal with it?

package com.scaffold.test.config;

import com.alibaba.fastjson.JSON;
import com.scaffold.test.base.Result;
import com.scaffold.test.base.ResultCode;
import com.scaffold.test.base.ServiceException;
import com.scaffold.test.config.interceptor.AuthenticationInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/ * * *@author alex
 */

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final Logger logger = LoggerFactory.getLogger(WebMvcConfigurer.class);

    /** * unified exception handling **@param exceptionResolvers
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        exceptionResolvers.add((request, response, handler, e) -> {
            Result result = new Result();
            // Exception handling
            // The parameter is abnormal
            if (e instanceof BindingResult) {
                StringBuilder errorMessage = new StringBuilder();
                List<ObjectError> allErrors = ((BindingResult) e).getAllErrors();
                for (int i = 0; i < allErrors.size(); i++) {
                    errorMessage.append(allErrors.get(i).getDefaultMessage());
                    if(i ! = allErrors.size() -1) {
                        errorMessage.append(",");
                    }
                }
                result.setCode(ResultCode.FAIL).setMessage(errorMessage.toString());
                logger.error(errorMessage.toString());
            } else if (e instanceof ServiceException) {
                // 1. Service failure exceptions, such as incorrect account or password
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
                logger.info(e.getMessage());
            } else if (e instanceof ServletException) {
                // 2. Call failed
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
            } else {
                // 3. Other internal errors
                result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage("Interface [" + request.getRequestURI() + "] Internal error, please contact administrator");
                String message;
                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = (HandlerMethod) handler;
                    message = String.format("Interface [%s] exception, method: %s.%s, exception Summary: %s",
                            request.getRequestURI(),
                            handlerMethod.getBean().getClass().getName(),
                            handlerMethod.getMethod().getName(),
                            e.getMessage());
                } else {
                    message = e.getMessage();
                }
                result.setMessage(message);
                logger.error(message, e);
            }
            responseResult(response, result);
            return new ModelAndView();
        });
    }

    // Process the response data format
    private void responseResult(HttpServletResponse response, Result result) {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type"."application/json; charset=UTF-8");
        response.setStatus(200);
        try {
            response.getWriter().write(JSON.toJSONString(result));
        } catch(IOException ex) { logger.error(ex.getMessage()); }}}Copy the code

Add a BindingResult judgment logic:

// The parameter is abnormal
if (e instanceof BindingResult) {
    StringBuilder errorMessage = new StringBuilder();
    List<ObjectError> allErrors = ((BindingResult) e).getAllErrors();
    for (int i = 0; i < allErrors.size(); i++) {
        errorMessage.append(allErrors.get(i).getDefaultMessage());
        if(i ! = allErrors.size() -1) {
            errorMessage.append(",");
        }
    }
    result.setCode(ResultCode.FAIL).setMessage(errorMessage.toString());
    logger.error(errorMessage.toString());
}
Copy the code

The effect is as follows:

The error Message returned is much nicer.

We’ve been using HTTP Get requests, so is there a problem with POST requests?

Let’s add a route /student/post:

    /** * Add student *@param student
     * @return* /
    @PostMapping("post")
    public Result postStudent(@Validated Student student) {
        return ResultGenerator.setSuccessResult(student);
    }
Copy the code

The current receiving mode needs to be used by the front-endFromDataorxxx-www-form-urlencodedPass parameters in the format of. We use thePostmanSimulate:

Parameter verification is normal;

What about using @requestBody to receive data?

@requestBody is used to receive data from the JSON string passed from the front end to the back end.

    /** * Add student *@param student
     * @return* /
    @PostMapping("post")
    public Result postStudent(@Validated @RequestBody Student student) {
        return ResultGenerator.setSuccessResult(student);
    }
Copy the code

The returned message is not formatted, so the returned Exception does not inherit from BindResult.

Using the @ RequestBody annotations, corresponding Excepiton type for MethodArgumentNotValidException;

So we need to modify the global exception detection code:

package com.scaffold.test.config;

import com.alibaba.fastjson.JSON;
import com.scaffold.test.base.Result;
import com.scaffold.test.base.ResultCode;
import com.scaffold.test.base.ServiceException;
import com.scaffold.test.config.interceptor.AuthenticationInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/ * * *@author alex
 */

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final Logger logger = LoggerFactory.getLogger(WebMvcConfigurer.class);

    /** * unified exception handling **@param exceptionResolvers
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        exceptionResolvers.add((request, response, handler, e) -> {
            Result result = new Result();
            // Exception handling
            // The parameter is abnormal
            if (e instanceof BindingResult || e instanceof MethodArgumentNotValidException) {
                StringBuilder errorMessage = new StringBuilder();
                List<ObjectError> allErrors;
                if (e instanceof BindingResult) {
                    allErrors = ((BindingResult) e).getAllErrors();
                } else {
                    BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
                    allErrors = bindingResult.getAllErrors();
                }
                for (int i = 0; i < allErrors.size(); i++) {
                    errorMessage.append(allErrors.get(i).getDefaultMessage());
                    if(i ! = allErrors.size() -1) {
                        errorMessage.append(",");
                    }
                }
                result.setCode(ResultCode.FAIL).setMessage(errorMessage.toString());
                logger.error(errorMessage.toString());
            } else if (e instanceof ServiceException) {
                // 1. Service failure exceptions, such as incorrect account or password
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
                logger.info(e.getMessage());
            } else if (e instanceof ServletException) {
                // 2. Call failed
                result.setCode(ResultCode.FAIL).setMessage(e.getMessage());
            } else {
                // 3. Other internal errors
                result.setCode(ResultCode.INTERNAL_SERVER_ERROR).setMessage("Interface [" + request.getRequestURI() + "] Internal error, please contact administrator");
                String message;
                if (handler instanceof HandlerMethod) {
                    HandlerMethod handlerMethod = (HandlerMethod) handler;
                    message = String.format("Interface [%s] exception, method: %s.%s, exception Summary: %s",
                            request.getRequestURI(),
                            handlerMethod.getBean().getClass().getName(),
                            handlerMethod.getMethod().getName(),
                            e.getMessage());
                } else {
                    message = e.getMessage();
                }
                result.setMessage(message);
                logger.error(message, e);
            }
            responseResult(response, result);
            return new ModelAndView();
        });
    }

    // Process the response data format
    private void responseResult(HttpServletResponse response, Result result) {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type"."application/json; charset=UTF-8");
        response.setStatus(200);
        try {
            response.getWriter().write(JSON.toJSONString(result));
        } catch(IOException ex) { logger.error(ex.getMessage()); }}}Copy the code

The message format is shown in the figure and has been converted correctly.

2.3. @Validated and @valid

So we have an @ “Validated” annotation, and there’s actually an @ “Valid” annotation;

So what's the difference between them?

The Validator interface has two interfaces, one is located in the javax.mail. Validation package, another is located in the org. Springframework. Validation package. Note that @valid is the former javax.validation, and @validated is the built-in validation interface of Spring.

“@” and “@” Valid have similar basic verification functions. But there are differences in annotations, nested validation, grouping, and so on.

2.3.1 Differences of annotations

@ Validated:

Can be used for types, methods, and method parameters. But not for member attributes (fields);

Nested validation cannot be provided independently on method entry parameters.

Cannot be used on member attributes (fields), nor can it prompt the framework for nested validation;

Can be nested with the @valid annotation for nested validation;

@ Valid:

Can be used on methods, constructors, method parameters, and member attributes (fields);

Nested validation cannot be provided independently on method entry parameters.

Can be used in member attributes (fields), prompting the validation framework for nested validation;

Can be nested with the @valid annotation for nested validation;

2.3.2 Differences of nesting functions

Suppose the Student entity class has a nested entity Mate as follows:

package com.scaffold.test.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.annotation.Id;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.List;

/ * * * *@author alex wong
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Student implements Serializable {

    private static final long serialVersionUID=1L;

    @Id
    @range (min = 1, message = "id cannot be empty ")
    private int id;

    @notblank (message = "name cannot be empty ")
    private String name;

    @notnull (message = "age cannot be empty ")
    private Integer age;

    // Partner list
    @notnull (message = "mateList cannot be empty ")
    @size (min = 1, message = "need at least one friend ")
    private List<Mate> mateList;

}
Copy the code
package com.scaffold.test.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

/** * partner *@author alex wong
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Mate implements Serializable {

    private static final long serialVersionUID=1L;

    @notblank (message = "user name cannot be blank ")
    private String name;

    @notnull (message = "The age of a friend cannot be empty ")
    private Integer age;
}
Copy the code
    /** * Add student *@param student
     * @return* /
    @PostMapping("post")
    public Result postStudent(@Validated @RequestBody Student student) {
        return ResultGenerator.setSuccessResult(student);
    }
Copy the code

At this time we do not change the other code, use the PostMan to http://192.168.66.65:9002/student/post:

@notnull (message = "mateList cannot be empty ")
Copy the code

@size (min = 1, message = "need at least one friend ")
Copy the code

As shown in the two figures above, whether @Validated or @valid ‘, the verification of the field is correct.

As shown in the figure above, whether @Validated or @valid, the fields in the Mate entity ‘cannot be verified in the current code.

So what to do?

@valid, this annotation is used to validate nested fields. “@”, “no”.

Nested validation must use @valid

package com.scaffold.test.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.annotation.Id;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.List;

/ * * * *@author alex wong
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Student implements Serializable {

    private static final long serialVersionUID=1L;

    @Id
    @range (min = 1, message = "id cannot be empty ")
    private int id;

    @notblank (message = "name cannot be empty ")
    private String name;

    @notnull (message = "age cannot be empty ")
    private Integer age;

    // Partner list
    @Valid // Nested validation must use @valid
    @notnull (message = "mateList cannot be empty ")
    @size (min = 1, message = "need at least one friend ")
    private List<Mate> mateList;

}
Copy the code

@valid The nested authentication succeeds

2.3.3, grouping

@ “Validated” : Supports the grouping function. Different authentication mechanisms can be used for input parameter verification based on different groups.

Valid: Groups are not supported.

For example:

A validation ID is required when updating a Student, but not when adding a new Student, so a different validation mechanism is required in this case.

So how to do that?

Define two interfaces

package com.scaffold.test.entity;

import javax.validation.groups.Default;

/** * Insert data group */
public interface Insert extends Default {}Copy the code
package com.scaffold.test.entity;

import javax.validation.groups.Default;

/** * Update data group */
public interface Update extends Default {}Copy the code

Then group the fields to be checked:

@Id
@range (min = 1, message = "id cannot be empty ", groups = update.class)
private int id;
Copy the code

Finally, add the @Validated annotation to the Controller processing request and introduce the groups to be verified as required

 /** * Add student *@param student
     * @return* /
    @PostMapping("post")
    public Result postStudent(@Validated(Insert.class) @RequestBody Student student) {
        return ResultGenerator.setSuccessResult(student);
    }

    /** * Update student *@param student
     * @return* /
    @PostMapping("update")
    public Result updateStudent(@Validated(Update.class) @RequestBody Student student) {
        return ResultGenerator.setSuccessResult(student);
    }
Copy the code

http://192.168.66.65:9002/student/post

http://192.168.66.65:9002/student/update

So far, packet validation is successful.

3, summarize

The unified design of the parameter verification of the overall project is an important part of the infrastructure, so that the colleagues of each writing business can avoid adding redundant codes to judge the parameters. Unified upper design, is a good choice.