Validators + Aspects: кастомизируем валидацию
Доброго времени суток, Хабр!
Спустя некоторое время решил вновь написать сюда и поделиться своим опытом. На этот раз статья будет о том, как кастомизировать стандартные валидаторы, и вызывать их там, где нам будет нужно, используя Spring — аспекты. Ну, а сподвигло меня на написание — практически отсутствие подобной информации, особенно на русском.
Проблема
Итак, суть приложения примерно такова: есть gateway — api, который принимает запрос, а в дальнейшем модифицирует и перенаправляет его соответствующему банку. Вот только запрос для каждого из банков отличался — как и параметры валидации. Поэтому валидировать изначальный запрос не представлялось возможным. Тут было два пути — использовать аннотации из javax.validation, либо писать свой отдельный слой валидации. В первом случае была загвоздка — по умолчанию объекты можно валидировать только в контроллере. Во втором случае так-же были минусы — это лишний слой, большое количество кода, да и в случае изменения моделей, пришлось бы менять и валидаторы.
Поэтому было принято решение найти способ дергать стандартные валидаторы там где это было необходимо, а не только в контроллере.
Дергаем валидаторы
Спустя пару часов копания в гугле были найдены пару решений, самое адекватное из которых было заавтовайрить javax.validation.Validator и вызвать у него метод validate, которому в качестве параметра нужно передать валидируемый объект.
Казалось бы, решение найдено, но автовайрить везде валидатор не казалось хорошей идеей, хотелось более элегантного решения.
Добавляем АОП
Недолго думая я решил попробовать адаптировать под это решение мною всеми любимые аспекты.
Логика была примерно следующей: создаём аннотацию, и вешаем её над методом который преобразует один объект в другой. Дальше в аспекте перехватываем все методы помеченные этой аннотацией и вызываем метод validate для возвращаемого ими значения. Профит.
Итак, аннотация:
// будет работать только для методов
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validate {}
Один из методов преобразующих запросы:
@Validate
public SomeBankRequest requestToBankRequest(Request request) {
SomeBankRequest bankRequest = ...;
...
// Преобразуем реквест в реквест для конкретного банка
...
return bankRequest;
}
Ну и собственно сам аспект:
@Aspect
@Component
public class ValidationAspect {
private final Validator validator;
// Автовайрим наш валидатор
public ValidationAspect(Validator validator) {
this.validator = validator;
}
// Перехватываем все точки вхождения нашей аннотации
// @Validate и объект возвращаемый помеченным ей методом
@AfterReturning(pointcut = "@annotation(api.annotations.Validate)", returning = "result")
public void validate(JoinPoint joinPoint, Object result) {
// Вызываем валидацию для объекта
Set> violations = validator.validate(result);
// Если сэт будет пустым, значит валидация прошла успешно, иначе в сэте будет // вся информация о полях не прошедших валидацию
if (!violations.isEmpty()) {
StringBuilder builder = new StringBuilder();
// берём нужную нам инфу и создаём из неё подходящее сообщение, проходя по // сэту
violations.forEach(violation -> builder
.append(violation.getPropertyPath())
.append("[" + violation.getMessage() + "],"));
throw new IllegalArgumentException("Invalid values for fields: " + builder.toString());
}
}
}
Коротко о работе аспекта:
Перехватываем объект возвращаемый методом, который помечен аннотацией Validate, дальше передаём его в метод валидатора, который вернёт нам Set
— если коротко — сэт классов с различной информацией о валидируемых полях и ошибках. В случае если ошибок не будет, то и сэт будет пустым. Дальше просто проходимся по сэту и создаём сообщение об ошибке, со всеми полями не прошедшими валидацию и выбрасываем экзепшн.
violation.getPropertyPath() - возвращает название поля
violation.getMessage() - конкретное сообщение, почему данное поле не прошло валидацию
Заключение
Таким образом, мы можем вызывать валидацию любых нужных нам объектов в любой точке приложения, а при желании можно дополнить аннотацию и аспект, чтобы валидация проходила не только для методов, возвращающих объект, но и для полей и параметров методов.
P.S.
Так-же если вызываете метод помеченный Validate из другого метода этого же класса, помните о связи аоп и прокси.