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 |
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-endFromData
orxxx-www-form-urlencoded
Pass parameters in the format of. We use thePostman
Simulate:
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.