[JAVA] Autoriser l'appel de valeurs arbitraires dans les messages d'erreur de validation de bean

Environnement de développement

introduction

Comment utiliser la validation de bean

Si vous voulez vérifier l'entrée avec «Java», c'est «Bean Validation». Il est facile à utiliser et pratique car il ajoute seulement une annotation: smile:. Créons maintenant une annotation qui vérifie si la valeur de la propriété est incluse dans le tableau de chaînes passées en argument, comme indiqué ci-dessous.

AcceptedStringValues.java


@Documented
@Constraint(validatedBy = {AcceptedStringValuesValidator.class})
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface AcceptedStringValues {
  String message() default "{com.neriudon.example.validator.AcceptedStringValues.message}";

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

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

  String[] value();

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

Validator vérifie si la chaîne passée à value () ʻ de l'annotation ʻAcceptedStringValues contient la valeur de retour du champ annoté ou de la méthode.

AcceptedStringValuesValidator.java


public class AcceptedStringValuesValidator
    implements ConstraintValidator<AcceptedStringValues, String> {

  // accepted values array
  private String[] validValues;

  @Override
  public void initialize(AcceptedStringValues constraintAnnotation) {
    validValues = constraintAnnotation.value();
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) {
      return true;
    }
    // check to exist value or not in accepted values array
    return Arrays.stream(validValues).anyMatch(s -> Objects.equals(value, s));
  }
}

Le code de test ressemble à ↓.

ValidationSampleApplicationTests.java


public class ValidationSampleApplicationTests {

  private ValidatorFactory validatorFactory;
  private Validator validator;

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

  @Test
  public void acceptedStringValuesNormal() throws UnsupportedEncodingException {
    AcceptedStringValuesSample sample = new AcceptedStringValuesSample("1");
    Set<ConstraintViolation<AcceptedStringValuesSample>> result = validator.validate(sample);
    // no error
    assertThat(result.isEmpty(), is(true));
  }

  @Test
  public void acceptedStringValuesNg() throws Exception {
    AcceptedStringValuesSample sample = new AcceptedStringValuesSample("0");
    Set<ConstraintViolation<AcceptedStringValuesSample>> result = validator.validate(sample);
    // error
    assertThat(result.size(), is(1));
    // assert error value and message
    result.stream().forEach(r -> {
      assertThat(r.getInvalidValue(), is("0"));
      assertThat(r.getMessage(), is("not accepted value."));
    });
  }

  private static class AcceptedStringValuesSample {

    @AcceptedStringValues({"1", "2", "3", "4", "5"})
    private String code;

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

Vous savez que les messages d'erreur sont automatiquement appelés si vous créez ValidationMessages.properties sous le chemin de classe et définissez le message avec la valeur spécifiée dans message de la classe d'annotation comme clé. Dans cet exemple, cela ressemble à ↓.

ValidationMessages.properties


com.neriudon.example.validator.AcceptedStringValues.message = not accepted values.

Incorporer la valeur dans le message d'erreur

Cela peut ressembler à ↑, mais si le message est corrigé, il peut être difficile à comprendre pour l'utilisateur. Dans Bean Validation, la valeur de propriété de la classe d'annotation peut être incorporée dans le message d'erreur, alors essayez de sortir la valeur définie dans la valeur de la classe d'annotation dans {valeur } comme une chaîne de caractères.

ValidationMessages.properties


com.neriudon.example.validator.AcceptedStringValues.message = not contained accepted values: {value}.

Le message "ne contient pas les valeurs acceptées: [1, 2, 3, 4, 5].".

Rendre les messages d'erreur plus intelligents

Bean Validation prend en charge les expressions EL à partir de la version 1.1! En spécifiant $ {validatedValue}, vous pouvez incorporer l'objet qui a causé l'erreur ...

ValidationMessages.properties


com.neriudon.example.validator.AcceptedStringValues.message = ${validatedValue} is not contained accepted values.

Cela entraînera le message «xxx ne contient pas les valeurs acceptées». (XXX est l'objet qui a causé l'erreur)

** Cependant, $ {validatedValue} renvoie l'objet par erreur tel quel, donc ne l'utilisez pas lorsque vous manipulez des informations confidentielles telles que des mots de passe. ** **

Problème de ne pas pouvoir intégrer d'autres baies

Eh bien, le sujet principal est d'ici. Jusqu'à présent, nous avons traité de String, mais avec ʻInteger` nous créons une annotation avec la même fonctionnalité.

AcceptedIntegerValues.java


@Documented
@Constraint(validatedBy = { AcceptedIntegerValuesValidator.class })
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
public @interface AcceptedIntegerValues {
	String message() default "{com.neriudon.example.validator.AcceptedIntegerValues.message}";

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

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

	int[] value();

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

AcceptedIntegerValuesValidator.java


public class AcceptedIntegerValuesValidator implements ConstraintValidator<AcceptedIntegerValues, Integer> {

	// accepted values array
	private Integer[] validValues;

	@Override
	public void initialize(AcceptedIntegerValues constraintAnnotation) {
		validValues = ArrayUtils.toObject(constraintAnnotation.value());
	}

	@Override
	public boolean isValid(Integer value, ConstraintValidatorContext context) {
		if (value == null) {
			return true;
		}
		// check to exist value or not in accepted values array
		return Arrays.stream(validValues).anyMatch(s -> Objects.equals(value, s));
	}
}

Définir un message d'erreur ...

ValidationMessages.properties


com.neriudon.example.validator.AcceptedIntegerValues.message = not contained accepted values: {value}.

Maintenant, testez

TestCode.java


	@Test
	public void acceptedIntegerValuesNormal() {
		AcceptedIntegerValuesSample sample = new AcceptedIntegerValuesSample(1);
		Set<ConstraintViolation<AcceptedIntegerValuesSample>> result = validator.validate(sample);
		assertThat(result.isEmpty(), is(true));
	}

	@Test
	public void acceptedIntegerValuesNg() {
		AcceptedIntegerValuesSample sample = new AcceptedIntegerValuesSample(0);
		Set<ConstraintViolation<AcceptedIntegerValuesSample>> result = validator.validate(sample);
		assertThat(result.size(), is(1));
		result.stream().forEach(r -> {
			assertThat(r.getInvalidValue(), is(0));
		});
	}
	private static class AcceptedIntegerValuesSample {

		@AcceptedIntegerValues({ 1, 2, 3, 4, 5 })
		private int code;

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

Alors ...

javax.validation.ValidationException: HV000149: An exception occurred during message interpolation
	at org.hibernate.validator.internal.engine.ValidationContext.interpolate(ValidationContext.java:477)
	at org.hibernate.validator.internal.engine.ValidationContext.createConstraintViolation(ValidationContext.java:322)
	at org.hibernate.validator.internal.engine.ValidationContext.lambda$createConstraintViolations$0(ValidationContext.java:279)
	at java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
	at java.util.Collections$2.tryAdvance(Unknown Source)
	at java.util.Collections$2.forEachRemaining(Unknown Source)
	at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(Unknown Source)
	at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
	at java.util.stream.ReferencePipeline.collect(Unknown Source)
	at org.hibernate.validator.internal.engine.ValidationContext.createConstraintViolations(ValidationContext.java:280)
	at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:182)
	at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:68)
	at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:73)
	at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:127)
	at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:120)
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:533)
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:496)
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:465)
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:430)
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:380)
	at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:169)
	at com.neriudon.example.ValidationSampleApplicationTests.acceptedIntegerValuesNg(ValidationSampleApplicationTests.java:98)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
Caused by: java.lang.ClassCastException: [I cannot be cast to [Ljava.lang.Object;
	at org.hibernate.validator.internal.engine.messageinterpolation.ParameterTermResolver.interpolate(ParameterTermResolver.java:30)
	at org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTerm.interpolate(InterpolationTerm.java:64)
	at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.interpolate(ResourceBundleMessageInterpolator.java:76)
	at org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateExpression(AbstractMessageInterpolator.java:385)
	at org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateMessage(AbstractMessageInterpolator.java:274)
	at org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolate(AbstractMessageInterpolator.java:220)
	at org.hibernate.validator.internal.engine.ValidationContext.interpolate(ValidationContext.java:468)
	... 55 more

Une erreur s'est produite: fatigué:. Il semble que la conversion depuis le tableau de ʻInteger de " [Je ne peux pas être convertie en [Ljava.lang.Object; "" "a échoué.

Essayons de créer une chaîne avec une expression EL.

ValidationMessages.properties


com.neriudon.example.validator.AcceptedIntegerValues.message = not contained accepted values: ${value.toString()}.

Quand je l'ai testé avec ceci, je n'ai pas eu d'erreur, mais cela ressemble à ceci:

not contained accepted values: [I@6e9319f. //Integer[]Édition
not contained accepted values: [1, 2, 3, 4, 5]. // String[]Édition

Donc, convertissons ʻInteger [] en String avec ʻArrays.toString.

ValidationMessages.properties


com.neriudon.example.validator.AcceptedIntegerValues.message = not contained accepted values: ${java.util.Arrays.toString(value)}.

Alors, quand vous l'exécutez ...

javax.el.PropertyNotFoundException: ELResolver cannot handle a null base Object with identifier [java]

Il n'y a pas d'objet comme java! J'ai eu l'erreur: déçu_relieved:.

Pour revenir au début, [Expression EL du message d'erreur Hibernate Validator 5](http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/ch04.html#section-interpolation- Lisons à propos de (with-message-expressions). Alors ...

The validation engine makes the following objects available in the EL context:

the attribute values of the constraint mapped to the attribute names the currently validated value (property, bean, method parameter etc.) under the name validatedValue a bean mapped to the name formatter exposing the var-arg method format(String format, Object…​ args) which behaves like java.util.Formatter.format(String format, Object…​ args).

Si vous le traduisez librement ...

Le moteur de validation peut utiliser les objets suivants dans les expressions EL.

  1. Valeur d'attribut définie dans l'annotation
  2. La valeur qui a réellement causé l'erreur
  3. Formatage basé sur java.util.Formatter.format (String format, Object… args)

En d'autres termes, rien d'autre ne peut être utilisé ...? Même si vous vous référez à l'exemple sous le lien, vous n'avez pas appelé la classe fournie par Java pour traiter le message. / (^ O ^) \ Nantekotai.

Permet d'appeler des valeurs arbitraires dans les messages d'erreur

Mais ce n'est pas sans chemin. Dans les conseils écrits sous l'exemple, il est écrit comme suit.

Only actual constraint attributes can be interpolated using message parameters in the form {attributeName}. When referring to the validated value or custom expression variables added to the interpolation context (see Section 11.9.1, “HibernateConstraintValidatorContext”), an EL expression in the form ${attributeName} must be used.

Si vous le traduisez librement ... Si vous définissez la valeur que vous souhaitez utiliser pour le contexte, vous pouvez y faire référence avec $ {}!

Et cela. Ensemble basé sur l'exemple écrit dans 11.9.1. HibernateConstraintValidatorContext Essayons.

Ajoutez le processus de conversion de ʻInterger [] en String et de le stocker dans le contexte comme ʻacceptedValuesToString à la méthode ʻisValid`.

AcceptedIntegerValuesValidator.java


	@Override
	public boolean isValid(Integer value, ConstraintValidatorContext context) {
		if (value == null) {
			return true;
		}
		// add acceptedValuesToString variable converted accepted integer values to string 
		context.unwrap(HibernateConstraintValidatorContext.class).addExpressionVariable("acceptedValuesToString", Arrays.toString(validValues));

		// check to exist value or not in accepted values array
		return Arrays.stream(validValues).anyMatch(s -> Objects.equals(value, s));
	}

Donc, si vous appelez $ {allowedValuesToString} dans ValidationMessages.properties ...

ValidationMessages.properties


com.neriudon.example.validator.AcceptedIntegerValues.message = not contained accepted values: ${acceptedValuesToString}.

J'ai reçu le message d'erreur «valeurs acceptées non contenues: [1, 2, 3, 4, 5].»! Je l'ai fait: stuck_out_tongue_closed_eyes:!

Résumé

Dans cet article, je vous ai montré comment autoriser les messages d'erreur de validation de bean à appeler des valeurs arbitraires. Cependant, cette méthode pose deux problèmes.

--Il est nécessaire d'expliquer à l'utilisateur les valeurs qui peuvent être utilisées dans le message d'erreur.

Étant donné que la fonction de validation par défaut est étendue, il n'y a pas de problème si vous la développez individuellement, mais si vous la publiez en tant que bibliothèque, par exemple, vous devez décrire quelles valeurs peuvent être utilisées dans JavaDoc, etc. ..

~~ Écrit en WARN sous la section HibernateConstraintValidatorContext, cette fonctionnalité elle-même est sujette à changement dans le futur. ~~

~~ Comme le dit la personne qui a écrit l'article, c'est une fonctionnalité qui ne doit pas être utilisée trop souvent: rolling_eyes:. ~~

Dans Hibernate Validator 6.0.13.Final, l'avertissement ci-dessus a été supprimé. Cela a-t-il été réparé?

Donc, si votre projet décide d'utiliser Hibernate Validator, cette fonctionnalité est utile (laquelle).

Recommended Posts

Autoriser l'appel de valeurs arbitraires dans les messages d'erreur de validation de bean
Comment afficher les messages d'erreur en japonais
Dans Bean Validation, si vous souhaitez inclure le nom du champ dans le message d'erreur, vous ne pouvez pas sortir de LocalValidatorFactoryBean.
[Rails] Où faire attention dans la description de la validation
[Spring Boot] Liste des règles de validation qui peuvent être utilisées dans le fichier de propriétés pour les messages d'erreur
[Rails] Erreur de validation inattendue dans l'appareil