Validators + Aspects: customize validation
- Tutorial
Good day, Habr!
After some time, I decided to write again here and share my experience. This time the article will be about how to customize the standard validators, and call them where we need them, using Spring - aspects. Well, it inspired me to write - almost no such information, especially in Russian.
So, the essence of the application is approximately as follows: there is a gateway - api, which accepts the request, and further modifies and redirects it to the appropriate bank. The only request for each of the banks was different - as were the validation parameters. Therefore, validating the original request was not possible. There were two ways - to use annotations from javax.validation, or to write your own separate layer of validation. In the first case there was a snag - by default, objects can be validated only in the controller. In the second case, there were also disadvantages - this is an extra layer, a large amount of code, and in the case of model changes, we would have to change the validators.
Therefore, it was decided to find a way to pull the standard validators where it was needed, and not just in the controller.
After a couple of hours of digging, a couple of solutions were found in Google, the most adequate of which was to customize javax.validation.Validator and call its validate method, which should be passed a validated object as a parameter.
It would seem that a solution was found, but to automatically type a validator everywhere did not seem like a good idea, I wanted a more elegant solution.
Without thinking twice, I decided to try to adaptmy favorite aspects to this decision .
The logic was something like this: create an annotation, and hang it over a method that converts one object into another. Further in the aspect we intercept all methods marked with this annotation and call the validate method for the value returned by them. Profit
So, the summary:
One of the methods for converting queries:
Well, actually the aspect itself:
Briefly about the work of the aspect: We
intercept the object returned by the method that is marked with the Validate annotation , then we transfer it to the validator method, which returns to us
Thus, we can call validation of any objects we need at any point of the application, and if you wish, you can add an annotation and an aspect so that validation takes place not only for the methods that return the object, but also for the fields and parameters of the methods.
Also, if you call a method labeled Validate from another method of the same class, remember the connection between aop and proxy .
After some time, I decided to write again here and share my experience. This time the article will be about how to customize the standard validators, and call them where we need them, using Spring - aspects. Well, it inspired me to write - almost no such information, especially in Russian.
Problem
So, the essence of the application is approximately as follows: there is a gateway - api, which accepts the request, and further modifies and redirects it to the appropriate bank. The only request for each of the banks was different - as were the validation parameters. Therefore, validating the original request was not possible. There were two ways - to use annotations from javax.validation, or to write your own separate layer of validation. In the first case there was a snag - by default, objects can be validated only in the controller. In the second case, there were also disadvantages - this is an extra layer, a large amount of code, and in the case of model changes, we would have to change the validators.
Therefore, it was decided to find a way to pull the standard validators where it was needed, and not just in the controller.
We pull validators
After a couple of hours of digging, a couple of solutions were found in Google, the most adequate of which was to customize javax.validation.Validator and call its validate method, which should be passed a validated object as a parameter.
It would seem that a solution was found, but to automatically type a validator everywhere did not seem like a good idea, I wanted a more elegant solution.
Add AOP
Without thinking twice, I decided to try to adapt
The logic was something like this: create an annotation, and hang it over a method that converts one object into another. Further in the aspect we intercept all methods marked with this annotation and call the validate method for the value returned by them. Profit
So, the summary:
// будет работать только для методов@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documentedpublic@interface Validate {}
One of the methods for converting queries:
@Validatepublic SomeBankRequest requestToBankRequest(Request<T> request){
SomeBankRequest bankRequest = ...;
...
// Преобразуем реквест в реквест для конкретного банка
...
return bankRequest;
}
Well, actually the aspect itself:
@Aspect@ComponentpublicclassValidationAspect{
privatefinal Validator validator;
// Автовайрим наш валидаторpublicValidationAspect(Validator validator){
this.validator = validator;
}
// Перехватываем все точки вхождения нашей аннотации // @Validate и объект возвращаемый помеченным ей методом@AfterReturning(pointcut = "@annotation(api.annotations.Validate)", returning = "result")
publicvoidvalidate(JoinPoint joinPoint, Object result){
// Вызываем валидацию для объекта
Set<ConstraintViolation<Object>> violations = validator.validate(result);
// Если сэт будет пустым, значит валидация прошла успешно, иначе в сэте будет // вся информация о полях не прошедших валидациюif (!violations.isEmpty()) {
StringBuilder builder = new StringBuilder();
// берём нужную нам инфу и создаём из неё подходящее сообщение, проходя по // сэту
violations.forEach(violation -> builder
.append(violation.getPropertyPath())
.append("[" + violation.getMessage() + "],"));
thrownew IllegalArgumentException("Invalid values for fields: " + builder.toString());
}
}
}
Briefly about the work of the aspect: We
intercept the object returned by the method that is marked with the Validate annotation , then we transfer it to the validator method, which returns to us
Set<ConstraintViolation<Object>>
- in short - the set of classes with various information about validated fields and errors. If there are no errors, then the set will be empty. Next, just go through the set and create an error message, with all the fields that have not passed validation and throw away the action.
violation.getPropertyPath() - возвращает название поля
violation.getMessage() - конкретное сообщение, почему данное поле не прошло валидацию
Conclusion
Thus, we can call validation of any objects we need at any point of the application, and if you wish, you can add an annotation and an aspect so that validation takes place not only for the methods that return the object, but also for the fields and parameters of the methods.
PS
Also, if you call a method labeled Validate from another method of the same class, remember the connection between aop and proxy .