J'essaie d'implémenter une logique de validation personnalisée pour un point de terminaison Spring Boot en combinant JSR-303 Bean Validation API
et Spring's Validator
.
Sur la base du diagramme de classe Validator, il semble possible d'étendre l'une des options CustomValidatorBean
, SpringValidatorAdapter
ou LocalValidatorFactoryBean
à l'ajout d'une logique de validation personnalisée à une méthode surchargée validate(Object target, Errors errors)
.
Cependant, si je crée un validateur qui étend l'une de ces trois classes et que je l'enregistre à l'aide de @InitBinder
, sa méthode validate(Object target, Errors errors)
n'est jamais appelée et aucune validation n'est effectuée. Si je supprime @InitBinder
, un validateur de ressort par défaut exécute le JSR-303 Bean Validation
.
Contrôleur de repos:
@RestController
public class PersonEndpoint {
@InitBinder("person")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new PersonValidator());
}
@RequestMapping(path = "/person", method = RequestMethod.PUT)
public ResponseEntity<Person> add(@Valid @RequestBody Person person) {
person = personService.save(person);
return ResponseEntity.ok().body(person);
}
}
Validateur personnalisé:
public class PersonValidator extends CustomValidatorBean {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
super.validate(target, errors);
System.out.println("PersonValidator.validate() target="+ target +" errors="+ errors);
}
}
Si mon validateur implémente org.springframework.validation.Validator
, sa méthode validate(Object target, Errors errors)
est appelée mais JSR-303 Bean Validation
n'est pas exécuté avant. Je peux implémenter ma validation JSR-303 personnalisée de la même manière que SpringValidatorAdapter
implémente son JSR-303 Bean Validation
mais il doit y avoir un moyen de l'étendre à la place:
@Override
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
J'ai envisagé d'utiliser des contraintes personnalisées JSR-303 pour éviter d'utiliser org.springframework.validation.Validator
dans son ensemble, mais il doit exister un moyen de faire fonctionner un validateur personnalisé.
Spring documentation de validation n’est pas très clair sur la combinaison des deux:
Une application peut également enregistrer d'autres instances Spring Validator par instance DataBinder, comme décrit à la Section 9.8.3, «Configuration d'un DataBinder». Cela peut être utile pour brancher la logique de validation sans utiliser d'annotations.
Et plus tard, il aborde la configuration de plusieurs instances de Validator
Un DataBinder peut également être configuré avec plusieurs instances de Validator via dataBinder.addValidators et dataBinder.replaceValidators. Ceci est utile lors de la combinaison de la validation de bean configurée globalement avec un validateur Spring configuré localement sur une instance de DataBinder. Voir ???.
J'utilise Spring Boot 1.4.0.
Per @ M.Deinum - utiliser addValidators () à la place de setValidator () a tout gâché. Je conviens également que l’utilisation de JSR-303, annotation basée sur la méthode @ AssertTrue spécifiquement pour la validation de champs croisés, est probablement une solution plus propre. Un exemple de code est disponible sur https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator . Dans l'exemple, la validation du deuxième prénom est effectuée via un validateur de ressort personnalisé, tandis que la validation du nom de famille est gérée par le validateur JSR 303 par défaut.
Ce problème peut être résolu en étendant LocalValidatorFactoryBean, vous pouvez remplacer la méthode validate
à l'intérieur de cette classe en donnant le comportement que vous souhaitez.
Dans mon cas, j’ai besoin d’utiliser JSR-303 ET des validateurs personnalisés pour le même modèle dans différentes méthodes du même contrôleur. Il est normalement recommandé d’utiliser @InitBinder, mais cela ne suffit pas dans mon cas, car InitBinder établit un lien entre Model et Validator (si vous utilisez @RequestBody InitBinder est juste pour un modèle et un validateur par contrôleur).
Manette
@RestController
public class LoginController {
@PostMapping("/test")
public Test test(@Validated(TestValidator.class) @RequestBody Test test) {
return test;
}
@PostMapping("/test2")
public Test test2(@Validated @RequestBody Test test) {
return test;
}
}
Validateur personnalisé
public class TestValidator implements org.springframework.validation.Validator {
@Override
public boolean supports(Class<?> clazz) {
return Test.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Test test = (Test) target;
errors.rejectValue("field3", "weird");
System.out.println(test.getField1());
System.out.println(test.getField2());
System.out.println(test.getField3());
}
}
Classe à valider
public class Test {
@Size(min = 3)
private String field2;
@NotNull
@NotEmpty
private String field1;
@NotNull
@Past
private LocalDateTime field3;
//...
//getter/setter
//...
}
CustomLocalValidatorFactoryBean
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void validate(@Nullable Object target, Errors errors, @Nullable Object... validationHints) {
Set<Validator> concreteValidators = new LinkedHashSet<>();
Set<Class<?>> interfaceGroups = new LinkedHashSet<>();
extractConcreteValidatorsAndInterfaceGroups(concreteValidators, interfaceGroups, validationHints);
proccessConcreteValidators(target, errors, concreteValidators);
processConstraintViolations(super.validate(target, interfaceGroups.toArray(new Class<?>[interfaceGroups.size()])), errors);
}
private void proccessConcreteValidators(Object target, Errors errors, Set<Validator> concreteValidators) {
for (Validator validator : concreteValidators) {
validator.validate(target, errors);
}
}
private void extractConcreteValidatorsAndInterfaceGroups(Set<Validator> concreteValidators, Set<Class<?>> groups, Object... validationHints) {
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
if (((Class<?>) hint).isInterface()) {
groups.add((Class<?>) hint);
} else {
Optional<Validator> validatorOptional = getValidatorFromGenericClass(hint);
if (validatorOptional.isPresent()) {
concreteValidators.add(validatorOptional.get());
}
}
}
}
}
}
@SuppressWarnings("unchecked")
private Optional<Validator> getValidatorFromGenericClass(Object hint) {
try {
Class<Validator> clazz = (Class<Validator>) Class.forName(((Class<?>) hint).getName());
return Optional.of(clazz.newInstance());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
logger.info("There is a problem with the class that you passed to "
+ " @Validated annotation in the controller, we tried to "
+ " cast to org.springframework.validation.Validator and we cant do this");
}
return Optional.empty();
}
}
Configurer l'application
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new CustomLocalValidatorFactoryBean();
}
}
Entrée sur /test
endpoint:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Sortie du point final /test
:
{
"timestamp": "2018-04-16T17:34:28.532+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"weird.test.field3",
"weird.field3",
"weird.Java.time.LocalDateTime",
"weird"
],
"arguments": null,
"defaultMessage": null,
"objectName": "test",
"field": "field3",
"rejectedValue": "2018-04-15T15:10:24",
"bindingFailure": false,
"code": "weird"
},
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.Java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "Não pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 2",
"path": "/user/test"
}
Entrée sur /test2
endpoint:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Sortie sur /test2
endpoint:
{
"timestamp": "2018-04-16T17:37:30.889+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.Java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "Não pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 1",
"path": "/user/test2"
}
J'espère que cette aide.