[Java] [Java] Creating original annotation

6 minute read

History

When I implemented it using many annotations in the initial model, I got a review saying “Because it is hard to see, it will be easier to see if you make your own annotations”, so I tried it.

before

contoller class

@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

/**
   * Sample data acquisition API
   *
   * @param id Sample data ID
   * @return Detailed information of 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

contoller class

@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

  /**
   * Sample data acquisition API
   *
   * @param id Sample data ID
   * @return Detailed information of 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;

}

Create annotation class

You can feel that the annotations are refreshed before/after. To realize this, first create an annotation class.

annotation class for id

@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 {};

}

Comment

@Target

In the argument of @Target, specify 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 retain the annotation.

| Value | range to hold | |:–|:–| | RetentionPolicy.RUNTIME | Hold at run time. | | RetentionPolicy.CLASS | Hold in class file. (It is not retained at run time.) | | RetentionPolicy.SOURCE | Keep in source file. (Not stored 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, the argument is empty because it is created by combining existing constraints.

constraint annotation

Add constraint annotations such as @Pattern and @Nonnull to the annotation class you created. The annotation given here is checked when the class is validated using the unique annotation.

@interface

You can define the annotation yourself by using @interface.

message

In message, specify the message when the constraint is violated.

groups

groups specifies the attribute to determine whether to execute the constraint check depending on the situation. By specifying the groups attribute, you can group the constraints into arbitrary groups, and by specifying that group when performing validation, you can check different constraints for each group. This time there is no need to group them, so it is implemented empty.

payload

payload specifies the attribute that gives an arbitrary category such as importance to constraint violation. It is used as needed, but this time there is no particular category, so it is implemented empty.

List

List is a nested annotation and is used when defining the same constraint 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.

*If you do not set the message, groups, and payload for the constraint annotation, a runtime error will occur.

Create other annotation classes

Similarly, create annotations for name and percentage.

name annotation class

@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 {};
}

percentage annotation class

@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 {};
}

Comment

@Size

Indicates that the string length of the target field is within the specified size.

@Digits

Specify the maximum number of digits in the integer part for integer and the maximum number of digits for the decimal part in fraction. @Digits(integer = 3, fraction = 2) means that “of the maximum number of digits 3, 2 digits after the decimal point are the maximum”.

Add #annotation to the used class

contoller class

@Validated
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/sample")
public class SampleController {

  /**
   * Sample data acquisition API
   *
   * @param id Sample data ID
   * @return Detailed information of 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;

}

important point

-Be sure to add @Validated to the class with your own annotation. -Add @Valid with your own annotation.

Bonus: Create your own validation process

You may want to add the validation process yourself instead of creating an annotation by combining existing constraints like this time. In that case, implement the validator class. I will implement the id validation process created this time.

annotation class for id

@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 process 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;
   }
 }

id class description

@Constraint

Set the validation process class in @Constraint.

IdValidator class explanation

ConstraintValidator<A,T>

Validators 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 id class is received in A and string value is received in T.

A is initialize and T is the argument type of isValid.

initialize

In initialize, you can get the attribute value of the constraint annotation set in the JavaBeans property. This time it is not necessary, so it is created empty.

isValid

Validate with isValid and if the return value is false, ConstraintVaiolation will be generated as a validation failure. The process is described here. This time, it judges whether the received value matches the regular expression and returns true or false. If it does not enter the if statement in the first place, it can be judged that the received value is not a String, so false is returned.

Add annotation to the used class

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 of 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, note the following carefully.

-Be sure to add @Validated to the class with your own annotation. -Add @Valid with your own annotation.

Summary

I was told that I could make my own annotations! I knew that I could make my own annotations. When I actually made it, the model class was 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. The annotation itself wasn’t that hard, but it took me quite a while to realize that it wouldn’t work unless you added @Validated or @Valid to the Controller or the class that uses the annotation.

References

Getting Started with Java EE7 (23)-Bean Validation Basics http://enterprisegeeks.hatenablog.com/entry/2016/02/15/072944

Get started with JavaEE7 (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