Введение в валидацию бобов
Проверка JavaBeans (Bean Validation) — это новая модель проверки, доступная как часть платформы Java EE 6. Модель Bean Validation поддерживается ограничениями в форме аннотаций, размещенных в поле, методе или классе компонента JavaBeans, такого как управляемый компонент.
Несколько встроенных ограничений доступны в пакете javax.validation.constraints . Учебник по Java EE 6 перечисляет все встроенные ограничения.
Ограничения в проверке bean-компонентов выражаются посредством аннотаций Java:
|
1
2
3
4
5
6
|
public class Person { @NotNull @Size(min = 2, max = 50) private String name; // ...} |
Bean Validation и RESTful веб-сервисы
JAX-RS 1.0 предоставляет отличную поддержку для извлечения значений запроса и связывания их с полями, свойствами и параметрами Java с помощью аннотаций, таких как @HeaderParam , @QueryParam и т. Д. Он также поддерживает связывание тел объектов запроса с объектами Java с помощью аннотированных параметров ( т.е. параметры, которые не аннотированы ни одной из аннотаций JAX-RS). В настоящее время любая дополнительная проверка этих значений в классе ресурсов должна выполняться программно.
В следующем выпуске, JAX-RS 2.0, содержится предложение, позволяющее объединять аннотации проверки с аннотациями JAX-RS. Например, учитывая аннотацию проверки @Pattern , в следующем примере показано, как можно проверять параметры формы.
|
1
2
3
4
5
6
7
8
|
@GET@Path("{id}")public Person getPerson( @PathParam("id") @Pattern(regexp = "[0-9]+", message = "The id must be a valid number") String id) { return persons.get(id);} |
Однако на данный момент единственным решением является использование собственной реализации. Далее представлено решение на основе инфраструктуры RESTEasy от JBoss, которое соответствует спецификации JAX-RS и добавляет интерфейс проверки RESTful через аннотацию @ValidateRequest .
Экспортированный интерфейс позволяет нам создавать нашу собственную реализацию. Тем не менее, уже есть один широко используемый и для которого RESTEasy также обеспечивает бесшовную интеграцию. Эта реализация — Hibernate Validator .
Этот провайдер может быть добавлен в проект через следующие зависимости Maven:
|
01
02
03
04
05
06
07
08
09
10
11
|
<dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxrs</artifactId> <version>2.3.2.Final</version> <scope>provided</scope></dependency><dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-hibernatevalidator-provider</artifactId> <version>2.3.2.Final</version></dependency> |
Замечания:
без объявления @ValidateRequest на уровне класса или метода проверка не произойдет, несмотря на применение аннотаций ограничений к методам, например, в примере выше.
|
1
2
3
4
5
6
7
8
9
|
@GET@Path("{id}")@ValidateRequestpublic Person getPerson( @PathParam("id") @Pattern(regexp = "[0-9]+", message = "The id must be a valid number") String id) { return persons.get(id);} |
После применения аннотации id параметра будет автоматически подтвержден при выполнении запроса.
Конечно, вы можете проверять целые объекты вместо отдельных полей, используя аннотацию @Valid .
Например, у нас может быть один метод, который принимает объект Person и проверяет его.
|
1
2
3
4
5
6
|
@POST@Path("/validate")@ValidateRequestpublic Response validate(@Valid Person person) { // ...} |
Замечания:
По умолчанию при сбое проверки контейнером выдается исключение, и клиенту возвращается состояние HTTP 500. Это поведение по умолчанию может / должно быть переопределено, что позволяет нам настраивать Response который возвращается клиенту через сопоставители исключений.
интернационализация
До сих пор мы использовали стандартные или жестко закодированные сообщения об ошибках, но это и плохая практика, и совсем не гибкая. I18N является частью спецификации Bean Validation и позволяет нам задавать пользовательские сообщения об ошибках, используя файл свойств ресурса. Имя файла ресурса по умолчанию — ValidationMessages.properties и должно включать пары свойств / значений, таких как:
|
1
2
|
person.id.pattern=The person id must be a valid numberperson.name.size=The person name must be between {min} and {max} chars long |
Примечание: {min} , {max} относятся к свойствам ограничения, с которым будет связано сообщение.
Эти определенные сообщения могут затем вводиться в ограничения проверки как:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@POST@Path("create")@Consumes(MediaType.APPLICATION_FORM_URLENCODED)public Response createPerson( @FormParam("id") @Pattern(regexp = "[0-9]+", message = "{person.id.pattern}") String id, @FormParam("name") @Size(min = 2, max = 50, message = "{person.name.size}") String name) { Person person = new Person(); person.setId(Integer.valueOf(id)); person.setName(name); persons.put(Integer.valueOf(id), person); return Response.status(Response.Status.CREATED).entity(person).build();} |
Чтобы обеспечить переводы на другие языки, необходимо создать новый файл ValidationMessages_XX.properties с переведенными сообщениями, где XX — код предоставляемого языка.
К сожалению, провайдер Hibernate Validator не поддерживает I18N на основе определенного HTTP-запроса. Он не учитывает HTTP-заголовок Accept-Language и всегда использует Locale по умолчанию, предоставленную Locale.getDefault() . Чтобы иметь возможность изменять Accept-Language HTTP-заголовка Accept-Language , должна быть предусмотрена пользовательская реализация.
Пользовательский поставщик валидатора
Приведенный ниже код предназначен для решения этой проблемы и был протестирован с JBoss AS 7.1 .
Первое, что нужно сделать, — это удалить resteasy-hibernatevalidator-provider Maven resteasy-hibernatevalidator-provider , поскольку мы предоставляем нашего собственного поставщика и добавить зависимость Hibernate Validator:
|
1
2
3
4
5
|
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.2.0.Final</version></dependency> |
Затем создайте пользовательский интерполятор сообщений, чтобы настроить используемый по умолчанию Locale стандарт.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
public class LocaleAwareMessageInterpolator extends ResourceBundleMessageInterpolator { private Locale defaultLocale = Locale.getDefault(); public void setDefaultLocale(Locale defaultLocale) { this.defaultLocale = defaultLocale; } @Override public String interpolate(final String messageTemplate, final Context context) { return interpolate(messageTemplate, context, defaultLocale); } @Override public String interpolate(final String messageTemplate, final Context context, final Locale locale) { return super.interpolate(messageTemplate, context, locale); }} |
Следующим шагом является предоставление ValidatorAdapter . Этот интерфейс был введен для отделения RESTEasy от реального API валидации.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
public class RESTValidatorAdapter implements ValidatorAdapter { private final Validator validator; private final MethodValidator methodValidator; private final LocaleAwareMessageInterpolator interpolator = new LocaleAwareMessageInterpolator(); public RESTValidatorAdapter() { Configuration<?> configuration = Validation.byDefaultProvider() .configure(); this.validator = configuration.messageInterpolator(interpolator) .buildValidatorFactory().getValidator(); this.methodValidator = validator.unwrap(MethodValidator.class); } @Override public void applyValidation(Object resource, Method invokedMethod, Object[] args) { // For the i8n to work, the first parameter of the method being validated must be a HttpHeaders if ((args != null) && (args[0] instanceof HttpHeaders)) { HttpHeaders headers = (HttpHeaders) args[0]; List<Locale> acceptedLanguages = headers.getAcceptableLanguages(); if ((acceptedLanguages != null) && (!acceptedLanguages.isEmpty())) { interpolator.setDefaultLocale(acceptedLanguages.get(0)); } } ValidateRequest resourceValidateRequest = FindAnnotation .findAnnotation(invokedMethod.getDeclaringClass() .getAnnotations(), ValidateRequest.class); if (resourceValidateRequest != null) { Set<ConstraintViolation<?>> constraintViolations = new HashSet<ConstraintViolation<?>>( validator.validate(resource, resourceValidateRequest.groups())); if (constraintViolations.size() > 0) { throw new ConstraintViolationException(constraintViolations); } } ValidateRequest methodValidateRequest = FindAnnotation.findAnnotation( invokedMethod.getAnnotations(), ValidateRequest.class); DoNotValidateRequest doNotValidateRequest = FindAnnotation .findAnnotation(invokedMethod.getAnnotations(), DoNotValidateRequest.class); if ((resourceValidateRequest != null || methodValidateRequest != null) && doNotValidateRequest == null) { Set<Class<?>> set = new HashSet<Class<?>>(); if (resourceValidateRequest != null) { for (Class<?> group : resourceValidateRequest.groups()) { set.add(group); } } if (methodValidateRequest != null) { for (Class<?> group : methodValidateRequest.groups()) { set.add(group); } } Set<MethodConstraintViolation<?>> constraintViolations = new HashSet<MethodConstraintViolation<?>>( methodValidator.validateAllParameters(resource, invokedMethod, args, set.toArray(new Class<?>[set.size()]))); if (constraintViolations.size() > 0) { throw new MethodConstraintViolationException( constraintViolations); } } }} |
Предупреждение: @HttpHeaders должен быть введен как первый параметр методов, которые будут проверены:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@POST@Path("create")@Consumes(MediaType.APPLICATION_FORM_URLENCODED)public Response createPerson( @Context HttpHeaders headers, @FormParam("id") @Pattern(regexp = "[0-9]+", message = "{person.id.pattern}") String id, @FormParam("name") @Size(min = 2, max = 50, message = "{person.name.size}") String name) { Person person = new Person(); person.setId(Integer.valueOf(id)); person.setName(name); persons.put(id, person); return Response.status(Response.Status.CREATED).entity(person).build();} |
Наконец, создайте провайдера, который выберет вышеупомянутые классы, которые будут использоваться для проверки ограничений Bean Validation:
|
01
02
03
04
05
06
07
08
09
10
11
|
@Providerpublic class RESTValidatorContextResolver implements ContextResolver<ValidatorAdapter> { private static final RESTValidatorAdapter adapter = new RESTValidatorAdapter(); @Override public ValidatorAdapter getContext(Class<?> type) { return adapter; }} |
Отображение исключений
API Bean Validation сообщает об ошибках, используя исключения типа javax.validation.ValidationException или любого из его подклассов. Приложения могут предоставлять настраиваемые поставщики сопоставления исключений для любого исключения. Реализация JAX-RS ДОЛЖНА всегда использовать провайдера, универсальный тип которого является ближайшим суперклассом исключения, причем определяемые приложением провайдеры имеют приоритет над встроенными провайдерами.
Отображение исключений может выглядеть так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@Providerpublic class ValidationExceptionMapper implements ExceptionMapper<MethodConstraintViolationException> { @Override public Response toResponse(MethodConstraintViolationException ex) { Map<String, String> errors = new HashMap<String, String>(); for (MethodConstraintViolation<?> methodConstraintViolation : ex .getConstraintViolations()) { errors.put(methodConstraintViolation.getParameterName(), methodConstraintViolation.getMessage()); } return Response.status(Status.PRECONDITION_FAILED).entity(errors) .build(); }} |
В приведенном выше примере показана реализация ExceptionMapper который отображает исключения типа MethodConstraintViolationException . Это исключение выдается реализацией Hibernate Validator, когда проверка одного или нескольких параметров метода, аннотированного @ValidateRequest завершается неудачно. Это гарантирует, что клиент получает отформатированный ответ, а не только исключение, распространяемое из ресурса.
Исходный код
Исходный код, используемый для этого поста, доступен на GitHub .
Предупреждение: убедитесь, что вы изменили имя файла свойств ресурса, чтобы файл ValidationMessages.properties (т. Locale.getDefault() Без суффикса) отображался в Locale как возвращено Locale.getDefault() .
Он является техническим руководителем в Present Technologies в Коимбре, Португалия, где он отвечает за стимулирование инноваций, обмена знаниями, обучения и выбора технологий. Сэмюэл является автором блога samaxes.com и пишет в Твиттере как @samaxes .
Ссылка: Сообщение об ошибке проверки подлинности bean-компонента JAX-RS от нашего партнера JCG Сэмюэля Сантоса в блоге Java Advent Calendar .