[JAVA] Je souhaite créer une annotation générique pour un type

Environnement de développement

introduction

Utilisez Bean Validation pour créer une annotation qui vérifie si la valeur saisie est incluse dans la liste définie par le code. L'utilisation est comme ↓.

Sample.java


public class Sample{

  @CodeExists(StringCode.class)
  private String code;

  :

Créer une classe de code

Le code est défini par Map (valeur de code, nom de code). Créez une méthode «existe» qui vérifie si la valeur saisie est définie par le code. De plus, créez la classe de code en une seule tonne pour qu'elle ne soit pas new. Il y a plusieurs façons d'implémenter singleton, mais j'ai utilisé la classe Holder en me référant au Design pattern" Singleton ".

StringCode.java



public class StringCode {

  public static final StringCode INSTANCE = StringCodeHolder.INSTANCE;

  private final Map<String, String> codeMap = createCodeMap();

  private StringCode() {}

  public boolean exists(String code) {
    return codeMap.containsKey(code);
  }

  private Map<String, String> createCodeMap() {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("1", "foo");
    map.put("2", "bar");
    map.put("3", "hage");
    return Collections.unmodifiableMap(map);
  }

  private static class StringCodeHolder {
    private static final StringCode INSTANCE = new StringCode();
  }
}

Mettre en œuvre le traitement de la vérification des entrées

La création d'une instance à partir de la classe de l'argument de l'annotation est bâclée, mais renvoie le résultat de la méthode ʻexists`.

CodeExistsValidator.java


public class CodeExistsValidator implements ConstraintValidator<CodeExists, String> {

  StringCode target;

  @Override
  public void initialize(CodeExists constraintAnnotation) {
    try {
      //Obtenir une instance de la classe constante cible
      target = (StringCode) (constraintAnnotation.value().getDeclaredField("INSTANCE").get(this));
    } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
        | SecurityException e) {
      //adapté
      e.printStackTrace();
    }
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    //Renvoie le résultat de la méthode exist
    return target.exists(value);
  }
}

Créer une annotation

Il n'y a rien de spécial à écrire ici.

CodeExists.java


@Documented
@Constraint(validatedBy = {CodeExistsValidator.class})
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
  String message() default "";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  Class<?> value();

  @Target({METHOD, FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    CodeExists[] value();
  }
}

Test d'annotation

Testons-le maintenant.

ValidationSampleApplicationTests.java


@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {

  private static final String EXIST_CODE = "1";
  private static final String NOT_EXIST_CODE = "5";

  private ValidatorFactory validatorFactory;
  private Validator validator;

  @Before
  public void setup() {
    validatorFactory = Validation.buildDefaultValidatorFactory();
    validator = validatorFactory.getValidator();
  }

  @Test
  public void existStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void notExistStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(NOT_EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.size(), is(1));
    assertThat(result.iterator().next().getInvalidValue(), is(NOT_EXIST_CODE));
  }

  /**
   *Classe avec code de type String
   */
  private static class StringCodeSample {
    @CodeExists(StringCode.class)
    private String code;

    public StringCodeSample(String code) {
      this.code = code;
    }
  }
}

Vous pouvez voir que le réglage «" 1 "», qui est défini par le code dans «StringCode.java», entraîne une erreur de vérification d'entrée de 0, et le réglage «" 5 "», qui n'est pas défini par le code, entraîne une erreur de vérification d'entrée.

Sujet principal

L'introduction est devenue très longue, mais ... Dans ↑, la définition du code était limitée à String, mais supposons que l'exigence de pouvoir définir le code même avec les types ʻint et boolean` soit ajoutée.

Appliquer l'interface à la définition de code

Créez une interface pour créer une classe de définition de code générique. La vérification de l'existence du code est courante quel que soit le type, définissez-la donc avec la méthode par défaut.

Code.java


public interface Code<T> {

  /**
   *Vérifiez si le code spécifié existe dans la liste de codes.
   */
  default boolean exists(T code) {
    return asMap().containsKey(code);
  }

  /**
   *Renvoie une carte qui lie les valeurs de code et les noms de code.
   */
  Map<T, String> asMap();
}

Correction de la définition du code

Modifiez le StringCode.java créé dans ↑.

--Implémenté Code <String> --Méthode ʻexists supprimée --ʻAsMap renvoie codeMap

StringCode.java


public class StringCode implements Code<String>{

  public static final StringCode INSTANCE = StringCodeHolder.INSTANCE;

  private final Map<String, String> codeMap = createCodeMap();

  private StringCode() {}

  @Override
  public Map<String, String> asMap() {
    return codeMap;
  }

  private Map<String, String> createCodeMap() {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("1", "foo");
    map.put("2", "bar");
    map.put("3", "hage");
    return Collections.unmodifiableMap(map);
  }

  private static class StringCodeHolder {
    private static final StringCode INSTANCE = new StringCode();
  }
}

Correction du traitement du contrôle d'entrée

Modifiez le CodeExistsValidator.java créé dans ↑.

CodeExistsValidator.java


public class CodeExistsValidator<T> implements ConstraintValidator<CodeExists, T> {

  Code<T> target;

  @Override
  public void initialize(CodeExists constraintAnnotation) {
    try {
      //Obtenir une instance de la classe constante cible
      target = convert(constraintAnnotation.value().getDeclaredField("INSTANCE").get(this));
    } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
        | SecurityException e) {
      //adapté
      e.printStackTrace();
    }
  }

  @Override
  public boolean isValid(T value, ConstraintValidatorContext context) {
    //Renvoie le résultat de la méthode exist
    return target.exists(value);
  }

  @SuppressWarnings("unchecked")
  private Code<T> convert(Object o) {
    return (Code<T>) o;
  }
}

Corriger l'annotation

Maintenant que nous avons modifié la définition du code et le processus de vérification des entrées, il ne nous reste plus qu'à modifier les annotations. Cependant, puisque le paramètre spécifié dans @ Constraint est une classe réelle qui implémente ConstraintValidator, il semble qu'il ne soit pas possible d'inclure un argument formel. Eh bien, c'est un problème même si vous spécifiez un type générique.

CodeExists.java


@Documented
@Constraint(validatedBy = {CodeExistsValidator.class}) // NG
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
  String message() default "";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  Class<? extends Code<?>> value();

  @Target({METHOD, FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    CodeExists[] value();
  }
}

Erreur de sortie

Type mismatch: cannot convert from Class<CodeExistsValidator> to Class<? extends ConstraintValidator<?,?>>

La classe de contrôle d'entrée pour chaque type peut être spécifiée dans @ Constraint

Cela aurait été bien si je pouvais écrire quelque chose comme @ Constraint (validatedBy = {CodeExistsValidator.class}), mais cela semble impossible ... Document Bean Validation 5.7.4. Algorithme de résolution ConstraintValidator J'ai remarqué en lisant (: //beanvalidation.org/latest-draft/spec/#constraintdeclarationvalidationprocess-validationroutine-typevalidatorresolution), mais la vérification d'entrée qui correspond à T ofConstraintValidator <A, T> ʻest Il semble y avoir un mécanisme à exécuter automatiquement. En d'autres termes, c'est OK si vous spécifiez la vérification d'entrée pourString et la vérification d'entrée pour ʻint.

Implémenter le traitement de vérification d'entrée pour String

Nous allons donc implémenter le processus de vérification d'entrée pour String qui hérite de CodeExistsValidator. Cependant, le contenu de traitement est déjà implémenté dans CodeExistsValidator, donc le contenu est vide.

StringCodeExistsValidator.java


public class StringCodeExistsValidator extends CodeExistsValidator<String> {
}

Dans l'annotation, spécifiez la classe d'implémentation.

CodeExists.java


@Documented
@Constraint(validatedBy = {StringCodeExistsValidator.class}) // OK
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {
  String message() default "";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  Class<? extends Code<?>> value(); //Seule la classe qui implémente Code

  @Target({METHOD, FIELD})
  @Retention(RUNTIME)
  @Documented
  public @interface List {
    CodeExists[] value();
  }
}

Implémenter le traitement de vérification d'entrée pour int

Créez une classe qui renvoie un Map avec un type ʻint` comme clé. Sauf pour le type, c'est la même chose que «String», donc seule une partie est extraite.

IntCode.java


public class IntCode implements Code<Integer> {

 :

  @Override
  public Map<Integer, String> asMap() {
    return codeMap;
  }

  private Map<Integer, String> createCodeMap() {
    Map<Integer, String> map = new LinkedHashMap<>();
    map.put(4, "foo");
    map.put(5, "bar");
    map.put(6, "hage");
    return Collections.unmodifiableMap(map);
  }

 :
}

Le processus de vérification d'entrée crée simplement une classe comme String et l'implémentation est vide. En outre, le CodeExistsValidator d'origine doit être une classe abstraite.

IntCodeExistsValidator.java


public class IntCodeExistsValidator extends CodeExistsValidator<Integer> {
}

Enfin, ajoutez la classe d'implémentation à l'annotation.

CodeExists.java


@Documented
@Constraint(validatedBy = {StringCodeExistsValidator.class, IntCodeExistsValidator.class}) //ici
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface CodeExists {

 :
}

Dans cet état, ajoutez un test.

ValidationSampleApplicationTests.java


@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationSampleApplicationTests {

  private static final String STRING_EXIST_CODE = "1";
  private static final String STRING_NOT_EXIST_CODE = "5";
  private static final int INT_EXIST_CODE = 4;
  private static final int INT_NOT_EXIST_CODE = 1;

  private ValidatorFactory validatorFactory;
  private Validator validator;

  @Before
  public void setup() {
    validatorFactory = Validation.buildDefaultValidatorFactory();
    validator = validatorFactory.getValidator();
  }

  @Test
  public void existStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(STRING_EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void notExistStringCode() throws Exception {
    StringCodeSample code = new StringCodeSample(STRING_NOT_EXIST_CODE);
    Set<ConstraintViolation<StringCodeSample>> result = validator.validate(code);
    assertThat(result.size(), is(1));
    assertThat(result.iterator().next().getInvalidValue(), is(STRING_NOT_EXIST_CODE));
  }

  @Test
  public void existIntCode() throws Exception {
    IntCodeSample code = new IntCodeSample(INT_EXIST_CODE);
    Set<ConstraintViolation<IntCodeSample>> result = validator.validate(code);
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void notExistIntCode() throws Exception {
    IntCodeSample code = new IntCodeSample(INT_NOT_EXIST_CODE);
    Set<ConstraintViolation<IntCodeSample>> result = validator.validate(code);
    assertThat(result.size(), is(1));
    assertThat(result.iterator().next().getInvalidValue(), is(INT_NOT_EXIST_CODE));
  }

  /**
   *Classe avec code de type String
   */
  private static class StringCodeSample {
    @CodeExists(StringCode.class)
    private String code;

    public StringCodeSample(String code) {
      this.code = code;
    }
  }

  /**
   *Classe avec code de type int
   */
  private static class IntCodeSample {
    @CodeExists(IntCode.class)
    private int code;

    public IntCodeSample(int code) {
      this.code = code;
    }
  }
}

Il a été confirmé que la définition de code pour la vérification d'entrée change en fonction de la classe spécifiée dans @ CodeExists. Cependant, à mesure que le nombre de types tels que «booléen» et «char» augmente, il est inévitable que le nombre de classes augmente.

Question publiée sur Stack Over Flow: Hibernate Validator - Add a Dynamic ConstraintValidator a également été utile.

Le code de cet article est publié sur GitHub.

Recommended Posts

Je souhaite créer une annotation générique pour un type
[Android] Je souhaite créer un ViewPager pouvant être utilisé pour les didacticiels
Je souhaite créer un formulaire pour sélectionner la catégorie [Rails]
Je veux créer un fichier Parquet même en Ruby
Je souhaite développer une application web!
Je souhaite rechercher de manière récursive des fichiers dans un répertoire spécifique
Je veux écrire un joli build.gradle
Je veux écrire un test unitaire!
Comment créer un référentiel Maven pour 2020
Je souhaite créer un SNS Web sombre avec Jakarta EE 8 avec Java 11
Comment créer une base de données H2 n'importe où
[Ruby] Je veux faire un saut de méthode!
Comment créer des pages pour le tableau "kaminari"
Je veux écrire une simple répétition d'une chaîne de caractères
J'ai essayé de créer une application de clonage LINE
Je souhaite concevoir une structure pour la gestion des exceptions
Une note que j'ai renoncé à essayer de créer une annotation personnalisée pour Lombok
[Azure] J'ai essayé de créer une application Java pour la création d'applications Web gratuites - [Débutant]
Je veux appeler une méthode d'une autre classe
java: Comment écrire une liste de types génériques [Note]
Je veux utiliser une petite icône dans Rails
[Spring Boot] Comment créer un projet (pour les débutants)
J'ai essayé de créer une compétence Clova en Java
Je souhaite surveiller un fichier spécifique avec WatchService
Je souhaite définir une fonction dans la console Rails
Je veux cliquer sur une broche GoogleMap dans RSpec
Je souhaite ajouter une fonction de suppression à la fonction de commentaire
[Azure] J'ai essayé de créer une application Java gratuitement ~ Se connecter avec FTP ~ [Débutant]
Je veux convertir des caractères ...
Comment créer une méthode
J'ai essayé de créer un environnement de développement java8 avec Chocolatey
Tutoriel pour créer un blog avec Rails pour les débutants Partie 1
[Java] Je souhaite convertir un tableau d'octets en un nombre hexadécimal
Je veux trouver un chemin relatif dans une situation où Path est utilisé
Comment créer une image de conteneur légère pour les applications Java
[Rails] J'ai essayé de créer une mini application avec FullCalendar
Je souhaite implémenter une fonction d'édition des informations produit ~ part1 ~
Je veux faire une liste avec kotlin et java!
Je veux appeler une méthode et compter le nombre
Je veux créer une fonction avec kotlin et java!
Même en Java, je veux afficher true avec un == 1 && a == 2 && a == 3
J'ai essayé de convertir une chaîne de caractères en un type LocalDate en Java
Je veux donner un nom de classe à l'attribut select
Je souhaite utiliser FireBase pour afficher une chronologie comme Twitter
J'ai essayé de créer un environnement de développement padrino avec Docker
Je souhaite renvoyer plusieurs valeurs de retour pour l'argument saisi
Comment créer et lancer un Dockerfile pour Payara Micro
Tutoriel pour créer un blog avec Rails pour les débutants Partie 0
Implémentation d'une API forte pour "Je veux afficher ~~ à l'écran" avec un simple CQRS
Je veux renvoyer un type différent de l'élément d'entrée avec Java8 StreamAPI Reduce ()
J'ai essayé de créer une application cartographique simple dans Android Studio
Je veux créer un bouton avec un saut de ligne avec link_to [Note]
Préparation à la création de l'application Rails
Je souhaite ajouter une fonction de navigation avec ruby on rails
Je souhaite utiliser le balayage arrière sur un écran qui utilise XLPagerTabStrip