Download this chapter source code
π₯π₯π₯ This chapter has been downloaded and shared on Github
preface
We often check data validity in the project, we often use to javax.mail. The validation. The constraints under the package annotations, such as @ NotBlank @ NotEmpty @ NotNull etc. We found that Spring does not provide a validation annotation for enumeration values, so this article will write one.
Yuan notes
Meta – annotations are annotations that explain other annotations. You can use meta – annotations to customize annotations. Java5 defines four annotations: @Documented, @Target, @Retention, and @Inherited.
Java 8 added @repeatable and @native annotations. These annotations can be found in the java.lang.annotation package. The functions and usage of each meta-annotation are described below.
@Documented
Documented is a marked annotation with no member variables. An annotation class decorated with @Documented annotation will be extracted into a document by the JavaDoc tool. By default, JavaDoc does not include annotations, but if @documented is specified when declaring an annotation, this is handled by a tool like JavaDoc, so the annotation type information is included in the generated help document.
@Target
The @target annotation is used to specify the scope of an annotation, that is, where the annotation modified by @target can be used. @ Target annotation is a member variable (value) is used to set the goal, the value is the Java lang. The annotation. The ElementType enum type array, commonly used in the table below for the ElementType enum constants.
The name of the | instructions |
---|---|
CONSTRUCTOR | Used of construction methods |
FIELD | For member variables (including enumeration constants) |
LOCAL_VARIABLE | Used for local variables |
METHOD | Method is used to |
PACKAGE | Used in the package |
PARAMETER | For type parameters (new in JDK 1.8) |
TYPE | Used for classes, interfaces (including annotation types), or enum declarations |
@Retention
@Retention describes the lifetime of the annotation, that is, how long the annotation is retained. @ the Retention of annotation member variables (value) is used to set up reserve strategy, the value is the Java lang. The annotation. RetentionPolicy enumerated types, RetentionPolicy have 3 enumerated constants, as shown below.
- SOURCE: valid in the SOURCE file (i.e. retained in the SOURCE file)
- CLASS: valid in a CLASS file (i.e., CLASS reserved)
- RUNTIME: valid at RUNTIME (i.e., reserved at RUNTIME)
SOURCE < CLASS < RUNTIME; If you need to retrieve annotation information dynamically at RUNTIME, use RUNTIME annotations only.
Use CLASS annotations if you want to do some pre-processing at compile time, such as generating auxiliary code (such as ButterKnife).
If you just do some checking operations, such as @Override and @SuppressWarnings, use the SOURCE annotation.
@Inherited
Inherited is a markup annotation that specifies that the annotation can be Inherited. Using the @Inherited Class annotation indicates that this annotation can be used with a child Class of the Class. This means that when a class uses an annotation decorated with @Inherited, its child classes automatically have the annotation.
@Repeatable
The @REPEATable annotation is a new addition to Java 8. It allows Repeatable annotations within the same program element, which is often needed when the same annotation needs to be used multiple times. Prior to Java 8, there could be at most one annotation of the same type in front of the same program element, and if you wanted to use multiple annotations of the same type in front of the same element, you had to use an annotation container.
Prior to Java 8:
public @interface Roles {
Role[] roles();
}
public @interface Role {
String roleName(a);
}
public class RoleTest {
@Roles(roles = { @Role(roleName = "role1"), @Role(roleName = "role2") })
public String doString(a){
return "hello"; }}Copy the code
Repeated annotations have been added since Java 8, and can be used as follows:
public @interface Roles {
Role[] value();
}
@Repeatable(Roles.class)
public @interface Role {
String roleName(a);
}
public class RoleTest {
@Role(roleName = "role1")
@Role(roleName = "role2")
public String doString(a){
return "hello"; }}Copy the code
The difference is that the @REPEATable annotation points to the stored annotation Roles so that the Role annotation can be reused directly when used. From the above example, using the @REPEATable annotation is more conventional and readable.
The results obtained by the two methods are the same. Repeated annotations are a simplification that is an illusion that multiple repeated annotations are actually handled by an array element that is the value member of a “container” annotation.
@Native
Using the @native annotation to modify a member variable indicates that the variable can be referenced by Native code, which is often used by code generation tools.
In actual combat
Add the dependent
Jsr-303 is Java’s standard framework for validation of Bean data. It defines a set of validation annotations that can be annotated on member variables, attribute methods, and so on.
Hibernate-validator provides an implementation of this standard. When developing web applications with Springboot, we introduce the spring-boot-starter-Web dependency. It introduces the spring-boot-starter-validation dependency by default, and spring-boot-starter-Validation references the Hibernate-Validator dependency.
In older versions of Springboot, spring-boot-starter-Web has removed the hibernate-Validator component contained in it. Please introduce hibernate-Validator separately
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.2. The Final</version>
</dependency>
Copy the code
If it is a Spring Boot2. x project, it is recommended to directly reference the following dependency to ensure that the version matches the version of SpringBoot. Otherwise, the @VALID failure may occur and the project will not report an error
Automatic springboot adaptation is recommended:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Copy the code
Actual combat 1: Verify whether the parameters are in the listed range
List the annotations
@Documented
@Constraint(validatedBy = {ListValusConstraintValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface ListValue {
// A message is displayed by default
String message(a) default"Parameter mismatch!";
// Default groupingClass<? >[] groups()default {};
// Default carrier
Class<? extends Payload>[] payload() default {};
/** * Custom * requires validation of String array *@return* /
String[] value();
/** * Custom * true Mandatory * false Non-mandatory **@return* /
boolean required(a) default true;
}
Copy the code
The @constraint annotation registers multiple validators for validation.
Annotation constraint validator
/ * * *@Description: Interger list Collection validator *@Author: jianweil
*/
public class ListValusConstraintValidator implements ConstraintValidator<ListValue.Object> {
private Set<Object> valus = new HashSet<>();
boolean required = true;
@Override
public void initialize(ListValue constraintAnnotation) {
try {
String[] value = constraintAnnotation.value();
for (String i : value) {
valus.add(i);
}
required = constraintAnnotation.required();
} catch (Exception e) {
throw new RuntimeException("Parameter verification conversion error", e); }}/** * Whether the verification is successful **@paramValue Specifies an annotation constraint validator * for both int and String@param context
* @return* /
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (required) {
if (Objects.isNull(value)) {
/ / the value is null
return false;
}
return valus.contains(value.toString());
} else {
// Value can be null
if (value == null) {
return true;
} else {
// If value is not null, check whether it is an enumerated value
returnvalus.contains(value.toString()); }}}}Copy the code
The Spring MVC framework will call the Initialize method to initialize the extension of the current annotation, and during the validation process will call isValid and return true if it passes, false if it doesn’t
If it does not pass, an exception is thrown, and the error message of the exception contains the message we defined earlier.
Request VO
/ * * *@Description: user VO *@Author: jianweil
* @date: 2022/3/1 10:03 * /
@Data
public class UserVO {
@notblank (message = "name cannot be blank ")
private String name;
@min (value = 18, message = "age < 18")
private int age;
/** * Enumerates gender values: 1 female and 2 male */
@ ListValue (value = {" 1 ", "2"}, message = "gender parameter wrong")
private Integer gender;
@notblank (message = "mailbox cannot be empty ")
@email (message = "Email format is not correct ")
private String email;
}
Copy the code
Controller @Valid Verification
@RestController
@RequestMapping("/test")
public class TestController {
/** * catch the data BindingResult. If the data format is wrong, it will not trigger a global exception catch@param vo
* @param rs
* @return* /
@PostMapping("/add")
public Object add(@RequestBody @Valid UserVO vo, BindingResult rs) {
// A typo
StringBuilder msg = new StringBuilder();
if (rs.hasErrors()) {
List<FieldError> fieldErrors = rs.getFieldErrors();
fieldErrors.forEach(item -> msg.append(item.getDefaultMessage()).append(";"));
return msg.toString();
}
return vo;
}
/** * Recommendation: Request data errors are caught by the global exception handler, which focuses on business **@param vo
* @return* /
@PostMapping("/add2")
public Object add2(@RequestBody @Valid UserVO vo) {
System.out.println("Test successful...");
returnvo; }}Copy the code
- In the controller, we can use BindingResult to inject into the method signature, springMVC will proxy the verification result for us, we can get the verification result directly, Add (@requestBody @valid UserVO vo, BindingResult rs)
At this time, if there is an exception, it will not be reported to interrupt the thread, we have to deal with it, because we have proxy this result, regardless of whether the verification is wrong. We generally don’t recommend this.
- Recommendation: Global exception catching is recommended for uniform handling, we only do business development, such as method add2(@requestBody @valid UserVO vo)
Global exception catching
/ * * *@Description: Global exception handler *@Author: jianweil
* @date: 2022/3/1 he * /
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/** * Interface parameter data format error */
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException");
StringBuilder msg = new StringBuilder();
e.getBindingResult().getAllErrors().forEach(item -> msg.append(item.getDefaultMessage()).append(";"));
// Simply return a string. You can encapsulate the return model in a uniform format
return msg.toString();
}
@ExceptionHandler({Exception.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Object handleException(Exception e) {
System.out.println(e);
returne.getMessage(); }}Copy the code
Request to test
# # # request right POST at http://localhost:8080/test/add2Content-Type: application/json {"name": "ljw","age": 18,"gender": 1,"email": "10086 @qq.com"} # # # error request POST at http://localhost:8080/test/add2Content-Type: application/json
{"name": "ljw","age": 18,"gender": 0,"email": "[email protected]"}
Copy the code
Practice 2: Use enumeration classes for verification
Above we have implemented a set of enumerations of all symbol requirements, using annotations for verification, but this method is not elegant, kind of like magic value, For example, @listValue (value = {“1”, “2”}, message = “wrong gender “) or @listValue (value = {“A”, “B”}, message =” wrong gender “), semantics are not clear.
In this project, we used enumerations instead of magic values. We can change enumeration to enumeration.
Enumeration annotations
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EnumValueConstraintValidator.class})
public @interface EnumValue {
// Default error message
String message(a) default "the integer is not one of the enum values";
// The group to which the constraint annotations belong at validation timeClass<? >[] groups()default {};
// Constrain the annotation's payload
Class<? extends Payload>[] payload() default {};
/** * The enumeration value needs to be verified **@return* /
Class<? extends Enum> value();
}
Copy the code
Annotation constraint validator
public class EnumValueConstraintValidator implements ConstraintValidator<EnumValue.Object> {
private Set<Object> values = new HashSet<>();
private Class<? extends Enum> enumClass;
boolean required = true;
/** * This method does some initialization verification **@param constraintAnnotation
*/
@Override
public void initialize(EnumValue constraintAnnotation) {
enumClass = constraintAnnotation.value();
try {
// Check whether the enum implements the getValue method
enumClass.getDeclaredMethod(ValidatorEnumMapper.METHOD_NAME);
for(Enum e : enumClass.getEnumConstants()) { Method declaredMethod = e.getClass().getDeclaredMethod(ValidatorEnumMapper.METHOD_NAME); Object obj = declaredMethod.invoke(e); values.add(obj); } required = constraintAnnotation.required(); }catch (NoSuchMethodException e) {
throw new IllegalArgumentException("the enum class has not getValue method", e);
} catch(Exception e) { e.printStackTrace(); }}/** * This method writes the specific verification logic: whether the verification number is in the range of the specified enumeration type **@param value
* @param constraintValidatorContext
* @return* /
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (required) {
if (Objects.isNull(value)) {
return false;
}
return isPass(value);
} else {
// Value can be null
if (Objects.isNull(value)) {
return true;
}
returnisPass(value); }}/** * if value is not null, check whether ** matches@param value
* @return* /
private boolean isPass(Object value) {
/ / not null
Enum[] enumConstants = enumClass.getEnumConstants();
if (enumConstants == null) {
// Return enumConstants = null if it is not an enumeration type
return true;
}
returnvalues.contains(value); }}Copy the code
The top interface
/ * * *@Description: enumerates the verification mapping interface *@Author: jianweil
* @date: 2022/3/1 and * /
public interface ValidatorEnumMapper<T> {
/** * has the same name as the following method */
public static final String METHOD_NAME = "getValue";
The /** * annotation constrains the validator to call this method to get the value of the enumeration **/
public T getValue(a);
}
Copy the code
Because enumerations can have multiple attributes, we need to compare the requested values to the attributes to be validated in the enumeration class, so we design an interface that the enumeration class implements abstract methods of that interface.
Enumeration class
public enum Gender implements ValidatorEnumMapper<Integer> {
male(1."Male"),
female(2."Female");
private int value;
private String type;
Gender(int value, String type) {
this.value = value;
this.type = type;
}
@Override
public Integer getValue(a) {
return this.value; }}Copy the code
Request VO
/ * * *@Description: user VO *@Author: jianweil
* @date: 2022/3/1 10:03 * /
@Data
public class UserVO {
@notblank (message = "name cannot be blank ")
private String name;
@min (value = 18, message = "age < 18")
private int age;
/** * Enumerates gender values: 1 female and 2 male */
@listValue (value = {"1", "2"}, message = "gender error ", Required = false)
private Integer gender;
@enumValue (value = gender.class, message = "Not in the range of enumeration supported by the system ",required = false)
private Integer genderEnum;
@notblank (message = "mailbox cannot be empty ")
@email (message = "Email format is not correct ")
private String email;
}
Copy the code
Controller @Valid Verification
@RestController
@RequestMapping("/test")
public class TestController {
/** * Recommendation: Request data errors are caught by the global exception handler, which focuses on business **@param vo
* @return* /
@PostMapping("/add2")
public Object add2(@RequestBody @Valid UserVO vo) {
System.out.println("Test successful...");
returnvo; }}Copy the code
Request to test
# # # request right POST at http://localhost:8080/test/add2Content-Type: application/json {"name": "ljw","age": 18,"gender": 1,"genderEnum": 1,"email": "10086 @qq.com"} # # # request right POST at http://localhost:8080/test/add2Content-Type: application/json {"name": "ljw","age": 18,"gender": 1,"genderEnum": null,"email": "10086 @qq.com"} # # # error request POST at http://localhost:8080/test/add2Content-Type: application/json
{"name": "ljw","age": 18,"gender": 1,"genderEnum": 3,"email": "[email protected]"}
Copy the code
Do you still use if to determine if a parameter is in an enumeration range? I’ve used the notes.
Pick up -@RestControllerAdvice and @controllerAdvice difference
The difference between @RestControllerAdvice and @ControllerAdvice is similar to the difference between @RestController and @Controller
The @RestControllerAdvice annotation contains the @ControllerAdvice annotation and the @responseBody annotation. So the method is one less @ResponseBody annotation
Write in the last
- ππ» : have harvest, praise encouragement!
- β€οΈ : Collect articles, easy to look back!
- π¬ : Comment exchange, mutual progress!