[JAVA] The story of encountering Spring custom annotation

Nice to meet you

This article is the 9th day article of MicroAd Advent Calendar 2017 . Since this was my first attempt at Qiita, I decided to keep a record as my own memo. Please let us know if you have any suggestions such as incorrect information or omissions.

goal

Let's make a tool that is easy but can be reused. Original annotation is a small step in that effort. And I know the contents a little.

Service logic is enough

~~ That's right ~~

I want to try something that appeals to me. For some reason @ This symbol has always seemed attractive. Also, when the same check logic is needed in the future, all you have to do is add annotations, and it is easy to see what kind of check will be performed.

Annotation (@interface)

For the time being, annotations in java are positioned as interfaces. According to quote from the link

The motivation for the introduction of annotations into the Java programming language was the need for semantic information for a given piece of code

It is described as coming out of the need to deal with the "opinion" of a given code in java programming.

The following are typical annotation overrides provided as standard APIs.

Override.java


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

The annotation attached to the annotation is This is a meta-annotation provided by the java.lang.annotation package.

@Target It is the target to be annotated. You can specify more than one by enclosing them in ``` {} . As the type of target

There are several, but in the case of @Override, the METHOD is targeted. In fact, @Override cannot be attached to elements other than methods.

@Retention Describes the range affected by annotations. Retention returns a RetentionPolicy, and there are only three types of RetentionPolicy.

In the case of @Override, RetentionPolicy is SOURCE, so At the end of compilation, it has no meaning == Bytecode conversion will not be performed.

In some cases, both Target and Retention are simply written as import static for readability.

This example

What I wanted this time was an annotation to count half-width and full-width characters separately </ b> and limit the maximum length </ b>. For the template, I generally referred to the javax.validation.constraints package (@Size and @NotNull, right?).

In fact, this original annotation, that is, Constraint Annotation, has a fixed template (message (), groups (), payload () must be set), so create it according to it.

I'll give it a name.

CustomSize.java


@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CustomSizeValidator.class})
public @interface CustomSize {

  String message() default "{validation.CustomSize.message}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  int max();

  @Target({FIELD})
  @Retention(RUNTIME)
  @Documented
  @interface List {
    CustomSize[] value();
  }
}

@Documented Indicates that writing with javadoc etc. is required to use this annotation. To be honest, I may not have needed it this time.

@Constraint Specify the class that describes the specific logic that you want to constrain (check) with this annotation. The following is the validation implementation class for this annotation.

CustomSizeValidator.java


public class CustomSizeValidator implements ConstraintValidator<CustomSize, String> {

  private int max;

  @Override
  public void initialize(CustomSize customSize) {
    max = customSize.max();
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    //CustomSizeUtil is a class that describes only check logic
    //The return value of getLength is int
    return CustomSizeUtil.getLength(value) <= max;
  }
}

initialize </ b> is the initialization process for calling isValid.

isValid </ b> implements the actual validation logic. The `<T> value` passed as a parameter is the actual object to be verified. This time, since it is a check of the length of the input character string, the String type character string corresponds to this.

If `` `value``` fails the check due to an incorrect value entered, false is returned.

message() This message is a warning (?) When an incorrect input is made. Contains the wording to be defined in the message properties (such as Hibernate's ValidationMessages.properties). Also, the key is written with a fully qualified class name.

For example, I think it will look like this.

ValidationMessages_jp.properties


validation.CustomSize.message = {0}Please enter so that it does not exceed

groups() It is a setting that can be customized for a specific validation group. Must be initialized with an empty Class <?> Type.

Grouping is for ordering constraints.

@GroupSequence({CheckA.class, CheckB.class}) //After checking A B
public interface GroupedChecks {
}

payload() It is a declaration only to give some meta information to the object to be checked. In fact, I think the contents of the javax.validation.Payload interface are empty and are for markers or categorization.

For example

CustomSize.java


@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CustomSizeValidator.class})
public @interface CustomSize {

  String message() default "{validation.CustomSize.message}";
  Class<?>[] groups() default {};
  class Text implements Payload{} // Add Payload
  // omit
}

SampleModel.java


public class SampleModel {
  @CustomSizeValidator(max = 20, payload = {CustomSizeValidator.Text.class})
  private String memo;
  // omit
}

It's not correct, but you can give meaning to the `memo``` field so that it can be identified as a categorization such as `Text```.

Leave empty unless otherwise noted.

max() This time, I set the name max because it is a validation that constrains the "maximum length".

 @CustomSizeValidator(max = 20) //Name to enter here
 private String memo;

List { A[] value() } Define the checkable target (s) depending on the implementation of ConstraintValidator.

@interface List {
  CustomSizeValidator value();
  AnotherValidator aaa();
}

Unless you have a specific reason, one element may be sufficient.

use

Same as other input checks, but add @Valid to the upper model passed as a controller parameter and define BindingResult by arranging it right next to it.

SampleController.java


  @RequestMapping(value = "/sample", method = RequestMethod.POST)
  public String samplePost(@ModelAttribute @Valid BigModel model, BindingResult result) {
    // omit
   }

For nested models, add @Valid to the upper model as well.

BigModel.java


public class BigModel {
  @Valid
  private SmallModel smallModel;
  // omit
}

This custom annotation is @Target ({ElementType.FIELD}), so it targets the field. Attach it to the field to be actually checked.

SmallModel.java


public class SmallModel {
  @CustomSize(max = 20)
  private String input;
  // omit
}

test

  • Test to run the app and try it directly (error is stored in `` `BindingResult```)

  • Test with test code (give some values and it really calculates exactly)

Since it is a character count with a fixed limit, I tested it with boundary value analysis. I'm testing the annotation constraint implementation class ```isValid () `` `.

CustomTest.groovy


   // omit
   when:
      def result = validator.isValid("", null)
    then:
      assert result == excepted
    where:
      testCase                | maxLen | doubleLen || excepted
      "Boundary value, small" | 5      | 4.5       || true
      "Boundary value, same"  | 5      | 5         || true
      "Boundary value, big"   | 5      | 5.5       || false

The end

It was easy to do something like this. I intended to finish it in about After all, it was very interesting to know the contents, and it was a very good opportunity to study.

reference

Hibernate Community Documentation

that's all

Recommended Posts