If I implemented it with many annotations in the original model, I got a review saying "It's hard to see, so if you make your own annotations and use it, it will be easier to see", so I tried it.
before
controller class
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {
/**
*Sample data acquisition API
*
* @param id sample data ID
* @return Detailed information on sample data
*/
@GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
@ResponseStatus(HttpStatus.OK)
public Response getSampleDataDetails(@Nonnull @Pattern(regexp = "[0-9]{8}")
@PathVariable final String id) {
log.info(String.format("SampleController { id : %s }", id));
return idUseCase.getSampleDataDetails(id);
}
}
model class
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ModelSample {
@NotBlank
@Size(max = 1024)
private String name;
@Digits(integer = 3, fraction = 2)
private Double percentage;
}
after
controller class
@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {
/**
*Sample data acquisition API
*
* @param id sample data ID
* @return Detailed information on sample data
*/
@GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
@ResponseStatus(HttpStatus.OK)
public Response getSampleDataDetails(@Id @Valid @Nonnull
@PathVariable final String id) {
log.info(String.format("SampleController { id : %s }", id));
return idUseCase.getSampleDataDetails(id);
}
}
model class
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Validated
public class ModelSample {
@Name
@Valid
private String name;
@Percentage
@Valid
private Double percentage;
}
You can feel that the annotations are refreshing before / after. To realize this, first create an annotation class.
ʻId annotation class`
@Target(ElementType.PARAMETER)
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Pattern(regexp = "[0-9]{8}")
@Nonnull
public @interface Id {
String message() default "id: don't match format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Target
The argument to @Target
specifies where to apply the annotation.
value | Where to apply |
---|---|
ElementType.TYPE | Class, interface, annotation, enum type |
ElementType.FIELD | field |
ElementType.CONSTRUCTOR | constructor |
ElementType.METHOD | Method |
@Retention
In the argument of @Retention
, specify the range to hold the annotation.
value | Range to hold |
---|---|
RetentionPolicy.RUNTIME | Hold at run time. |
RetentionPolicy.CLASS | Keep in class file. (Not retained at runtime.) |
RetentionPolicy.SOURCE | Keep it in the source file. (It is not retained in the class file.) |
If you do not add * @Retention, RetentionPolicy.CLASS
will be the default.
@Constraint
In the argument of @ Constraint
, validateBy attribute, specify the validation to be executed by this annotation. If you want to implement your own validation process, specify the validator class that implements the process here. This time, we will create a combination of existing constraints, so this argument is left empty.
Add constraint annotations such as @Pattern
and @Nonnull
to your own annotation class. The annotation added here is checked when validating the class using the original annotation.
@interface
You can define your own annotations by using @ interface
.
message
For message
, specify the message when the constraint is violated.
groups
groups
specifies an attribute to determine whether to execute a constraint check depending on the situation. The groups attribute allows you to group constraints into any group, and you can specify that group when performing validation to check different constraints for each group. This time, it is not necessary to group them, so it is implemented empty.
payload
payload
specifies an attribute that gives an arbitrary category, such as severity, for constraint violations. I will use it as needed, but this time there is no particular category, so I am implementing it empty.
List
List
is a nested annotation and is used when the same constraint is defined multiple times under different conditions. This time, we will not use the same constraint because we will not define it multiple times under different conditions.
Similarly, create annotations for name
and percentage
.
Annotation class for name
@Target(ElementType.FIELD)
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Size(max = 1024)
@NotBlank
public @interface Name {
String message() default "name: don't match format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
`Annotation class for percentage``
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Digits(integer = 3, fraction = 2)
public @interface Percentage {
String message() default "percentage: don't match format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Size Indicates that the string length of the target field is within the specified size range.
@Digits
For ʻinteger, specify the maximum number of digits for the integer part, and for
fraction, specify the maximum number of digits for the decimal part.
@Digits (integer = 3, fraction = 2)` means that "of the maximum number of digits of 3, 2 digits after the decimal point are the maximum".
controller class
@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {
/**
*Sample data acquisition API
*
* @param id sample data ID
* @return Detailed information on sample data
*/
@GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
@ResponseStatus(HttpStatus.OK)
public Response getSampleDataDetails(@Id @Valid @Nonnull
@PathVariable final String id) {
log.info(String.format("SampleController { id : %s }", id));
return idUseCase.getSampleDataDetails(id);
}
}
model class
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Validated
public class ModelSample {
@Name
@Valid
private String name;
@Percentage
@Valid
private Double percentage;
}
--Be sure to add @Validated
to the class to which you want to add your own annotation.
--Add @Valid
along with your own annotation.
You may want to add your own validation process instead of combining existing constraints to create annotations, as in this case. In such a case, implement the validator class. We will implement the id validation process created this time.
ʻId annotation class`
@Target(ElementType.PARAMETER)
@Retention(RUNTIME)
@Constraint(validatedBy = {idValidator.class})
@Pattern(regexp = "[0-9]{8}")
@Nonnull
public @interface Id {
String message() default "id: don't match format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validation processing implementation class
public class IdValidator implements ConstraintValidator<id, String> {
@Override
public void initialize(Id id) {}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
String pattern = "[0-9]{8}";
if (!StringUtils.isEmpty(value)) {
return value.matches(pattern);
}
return false;
}
}
@Constraint
Set the validation processing class in @ Constraint
.
ConstraintValidator<A,T>
The validator must implement ConstraintValidator <A, T>
. A is the constraint annotation and T is the input value type. This time, I created the constraint annotation by myself, so I implemented it on the assumption that A will receive the id class and T will receive the value as a String.
A is initialize and T is the argument type of isValid.
initialize ʻInitialize` allows you to get the attribute value of the constraint annotation set in the JavaBeans property. I don't need it this time, so I created it empty.
isValid If validation is done with ʻisValid` and the return value is false, ConstraintVaiolation will be generated as validation failure. The process will be described here. This time, it determines if the received value matches the regular expression and returns true or false. If it is not included in the if statement in the first place, it can be determined that the received value is not a String, so false is returned.
The usage is the same as when creating by combining existing annotations.
@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {
/**
*Sample data acquisition API
*
* @param id sample data ID
* @return Detailed information on sample data
*/
@GetMapping(path = "{id}", produces = "application/json; charset=UTF-8")
@ResponseStatus(HttpStatus.OK)
public Response getSampleDataDetails(@Id @Valid @Nonnull
@PathVariable final String id) {
log.info(String.format("SampleController { id : %s }", id));
return idUseCase.getSampleDataDetails(id);
}
}
Again, please be careful about the following.
--Be sure to add @Validated
to the class to which you want to add your own annotation.
--Add @Valid
along with your own annotation.
I was told, "Make your own annotations!" And learned that you can make your own annotations. When I actually made it, the model class became much cleaner and easier to see. I thought it was a merit that it became easier to understand what kind of validation was applied to each. Implementing annotations wasn't too difficult, but it took me a while to realize that it wouldn't work without the @Validated
and @Valid
s in Controller and the classes that use annotations.
Getting Started with Java EE 7 (23)-Basics of Bean Validation http://enterprisegeeks.hatenablog.com/entry/2016/02/15/072944
Get started with Java EE 7 (24)-Create custom constraints with Bean Validation http://enterprisegeeks.hatenablog.com/entry/2016/02/29/072811
Functional details of TERASOLUNA Global Framework --5.5 Input check https://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/Validation.html#validation-basic-validation
Recommended Posts