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;
:
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();
}
}
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);
}
}
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();
}
}
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.
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.
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.
String
.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();
}
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();
}
}
Modifiez le CodeExistsValidator.java
créé dans ↑.
ConstraintValidator
de String
à T
cible
en Code <T>
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;
}
}
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<?,?>>
@ 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 pour
String et la vérification d'entrée pour ʻint
.
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();
}
}
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