Статьи

Интернационализация сообщения об ошибке проверки bean-компонента JAX-RS

Введение в валидацию бобов

Проверка 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}")
@ValidateRequest
public 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")
@ValidateRequest
public 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 number
person.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
@Provider
public 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
@Provider
public 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 .