Статьи

Проверка данных ресурса JAX-RS с помощью проверки компонентов в Java EE 7 и WildFly

Я уже подходил к этой теме дважды в прошлом. Во-первых, в моей статье « Интеграция проверки компонентов с помощью JAX-RS в Java EE 6» , описывающей, как использовать проверку компонентов с помощью JAX-RS в JBoss AS 7, даже до того, как это было определено в спецификации платформы Java EE . Позже, в статье, написанной для JAX Magazine и опубликованной позже в JAXenter , с использованием нового стандартного способа, определенного в Java EE 7 с сервером Glassfish 4 (первый сертифицированный сервер Java EE 7).
Теперь, когда WildFly 8 , ранее известный как JBoss Application Server, наконец-то достиг окончательной версии и присоединился к клубу сертифицированных серверов Java EE 7, пришло время для нового поста, освещающего особенности и различия между этими двумя серверами приложений, GlassFish 4 и WildFly 8.

Спецификации и API

Java EE 7 — долгожданный капитальный ремонт Java EE 6. С каждым выпуском Java EE добавляются новые функции и улучшаются существующие спецификации. Java EE 7 основана на успехе Java EE 6 и продолжает фокусироваться на повышении производительности труда разработчиков.

JAX-RS , Java API для веб-сервисов RESTful, является одним из самых быстро развивающихся API в ландшафте Java EE. Это, конечно, связано с массовым внедрением веб-сервисов на основе REST и увеличением числа приложений, которые используют эти сервисы.

В этом посте будут рассмотрены шаги, необходимые для настройки конечных точек REST для поддержки клиента JavaScript и обработки исключений из проверки для отправки локализованных сообщений об ошибках клиенту в дополнение к кодам ошибок HTTP.

Исходный код

Исходный код, сопровождающий эту статью, доступен на GitHub .

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

Проверка JavaBeans ( Bean Validation ) — это новая модель проверки, доступная как часть платформы Java EE 6. Модель Bean Validation поддерживается ограничениями в форме аннотаций, размещенных в поле, методе или классе компонента JavaBeans, такого как управляемый компонент.

Несколько встроенных ограничений доступны в пакете javax.validation.constraints . Учебное пособие по Java EE 7 содержит список всех этих ограничений.

Ограничения в проверке 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 обеспечивает отличную поддержку для извлечения значений запроса и связывания их с полями, свойствами и параметрами Java с использованием аннотаций, таких как @HeaderParam , @QueryParam и т. Д. Он также поддерживает связывание тел объектов запроса с объектами Java с помощью неаннотированных параметров (т.е. , параметры, не аннотированные ни одной из аннотаций JAX-RS). Однако до JAX-RS 2.0 любая дополнительная проверка этих значений в классе ресурсов должна была выполняться программно.

Последний выпуск, 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);
}

Помимо проверки отдельных полей, вы также можете проверять целые объекты с @Valid аннотации @Valid .
В качестве примера, метод ниже получает объект Person и проверяет его:

1
2
3
4
@POST
public Response validatePerson(@Valid Person person) {
    // ...
}

интернационализация

В предыдущем примере мы использовали сообщения об ошибках по умолчанию или жестко закодированные, но это и плохая практика, и совсем не гибкая. I18n является частью спецификации Bean Validation и позволяет нам задавать пользовательские сообщения об ошибках, используя файл свойств ресурса. Имя файла ресурса по умолчанию — ValidationMessages.properties и должно включать пары свойств / значений, таких как:

1
2
3
person.id.notnull=The person id must not be null
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
17
@POST
@Path("create")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createPerson(
        @FormParam("id")
        @NotNull(message = "{person.id.notnull}")
        @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();
}

Чтобы обеспечить переводы на другие языки, необходимо создать новый файл ValidationMessages_XX.properties с переведенными сообщениями, где XX — код предоставляемого языка.

К сожалению, на некоторых серверах приложений поставщик Validator по умолчанию не поддерживает i18n на основе определенного HTTP-запроса. Они не учитывают HTTP-заголовок Accept-Language и всегда используют Locale по умолчанию, предусмотренную Locale.getDefault() . Чтобы иметь возможность изменять Accept-Language HTTP-заголовка Accept-Language (который соответствует языку, настроенному в параметрах браузера), необходимо предоставить пользовательскую реализацию.

Пользовательский провайдер Validator

Хотя WildFly 8 правильно использует HTTP-заголовок Accept-Language для выбора правильного пакета ресурсов, другие серверы, такие как GlassFish 4, не используют этот заголовок. Поэтому для полноты и более простого сравнения с кодом GlassFish (доступным в рамках того же проекта GitHub ) я также реализовал собственный поставщик Validator для WildFly.
Если вы хотите увидеть пример GlassFish, посетите страницу Интеграция проверки бинов с JAX-RS в JAXenter.

  1. Добавить зависимость RESTEasy в Maven
  2. WildFly использует RESTEasy , реализацию JBoss спецификации JAX-RS.
    Зависимости RESTEasy необходимы для провайдера Validator и Exception Mapper, о которых пойдет речь далее в этом посте. Добавим это в Maven:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-bom</artifactId>
                <version>3.0.6.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
     
    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jaxrs</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-validator-provider-11</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

  3. Создайте ThreadLocal для сохранения Locale из заголовка HTTP Accept-Language
  4. Переменные ThreadLocal отличаются от своих обычных аналогов тем, что каждый поток, который обращается к одному, имеет свою собственную, независимо инициализированную копию переменной.

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
     * {@link ThreadLocal} to store the Locale to be used in the message interpolator.
     */
    public class LocaleThreadLocal {
     
        public static final ThreadLocal<Locale> THREAD_LOCAL = new ThreadLocal<Locale>();
     
        public static Locale get() {
            return (THREAD_LOCAL.get() == null) ? Locale.getDefault() : THREAD_LOCAL.get();
        }
     
        public static void set(Locale locale) {
            THREAD_LOCAL.set(locale);
        }
     
        public static void unset() {
            THREAD_LOCAL.remove();
        }
    }

  5. Создайте фильтр запросов для чтения HTTP-заголовка Accept-Language
  6. Фильтр запросов отвечает за чтение первого языка, отправленного клиентом в HTTP-заголовке Accept-Language и сохранение Accept-Language стандарта в нашем ThreadLocal :

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    /**
     * Checks whether the {@code Accept-Language} HTTP header exists and creates a {@link ThreadLocal} to store the
     * corresponding Locale.
     */
    @Provider
    public class AcceptLanguageRequestFilter implements ContainerRequestFilter {
     
        @Context
        private HttpHeaders headers;
     
        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            if (!headers.getAcceptableLanguages().isEmpty()) {
                LocaleThreadLocal.set(headers.getAcceptableLanguages().get(0));
            }
        }
    }

  7. Создайте пользовательский интерполятор сообщений для принудительного применения определенной Locale
  8. Затем создайте пользовательский интерполятор сообщений, чтобы применить конкретное значение Locale , обходя или переопределяя стратегию Locale по умолчанию:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
     * Delegates to a MessageInterpolator implementation but enforces a given Locale.
     */
    public class LocaleSpecificMessageInterpolator implements MessageInterpolator {
     
        private final MessageInterpolator defaultInterpolator;
     
        public LocaleSpecificMessageInterpolator(MessageInterpolator interpolator) {
            this.defaultInterpolator = interpolator;
        }
     
        @Override
        public String interpolate(String message, Context context) {
            return defaultInterpolator.interpolate(message, context, LocaleThreadLocal.get());
        }
     
        @Override
        public String interpolate(String message, Context context, Locale locale) {
            return defaultInterpolator.interpolate(message, context, locale);
        }
    }

  9. Настройте провайдера валидатора
  10. RESTEasy получает реализацию Bean Validation, ища провайдера, реализующего ContextResolver<GeneralValidator> .
    Чтобы настроить нового поставщика услуг валидации для использования нашего пользовательского интерполятора сообщений, добавьте следующее:

    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
    /**
     * Custom configuration of validation. This configuration can define custom:
     * <ul>
     * <li>MessageInterpolator - interpolates a given constraint violation message.</li>
     * <li>TraversableResolver - determines if a property can be accessed by the Bean Validation provider.</li>
     * <li>ConstraintValidatorFactory - instantiates a ConstraintValidator instance based off its class.
     * <li>ParameterNameProvider - provides names for method and constructor parameters.</li> *
     * </ul>
     */
    @Provider
    public class ValidationConfigurationContextResolver implements ContextResolver<GeneralValidator> {
     
        /**
         * Get a context of type {@code GeneralValidator} that is applicable to the supplied type.
         *
         * @param type the class of object for which a context is desired
         * @return a context for the supplied type or {@code null} if a context for the supplied type is not available from
         *         this provider.
         */
        @Override
        public GeneralValidator getContext(Class<?> type) {
            Configuration<?> config = Validation.byDefaultProvider().configure();
            BootstrapConfiguration bootstrapConfiguration = config.getBootstrapConfiguration();
     
            config.messageInterpolator(new LocaleSpecificMessageInterpolator(Validation.byDefaultProvider().configure()
                    .getDefaultMessageInterpolator()));
     
            return new GeneralValidatorImpl(config.buildValidatorFactory(),
                    bootstrapConfiguration.isExecutableValidationEnabled(),
                    bootstrapConfiguration.getDefaultValidatedExecutableTypes());
        }
    }

Отображение исключений

По умолчанию, когда проверка не проходит, контейнер генерирует исключение, и клиенту возвращается ошибка HTTP.

Спецификация Bean Validation определяет небольшую иерархию исключений (все они наследуются от ValidationException ), которые могут быть выброшены во время инициализации механизма проверки или (в нашем случае более важно) во время проверки значений ввода / вывода ( ConstraintViolationException ). Если выброшенное исключение является подклассом ValidationException за исключением ConstraintViolationException тогда это исключение сопоставляется с ответом HTTP с кодом состояния 500 (Внутренняя ошибка сервера). С другой стороны, когда создается ConstraintViolationException возвращаются два разных кода состояния:

  • 500 — внутренняя ошибка сервера)
    Если исключение было сгенерировано во время проверки типа возвращаемого метода.
  • ошибка 400, неверный запрос)
    В противном случае.

К сожалению, WildFly вместо исключения исключения ConstraintViolationException для недопустимых значений ввода создает ResteasyViolationException , которое реализует интерфейс ValidationException .
Это поведение можно настроить, чтобы мы могли добавлять сообщения об ошибках в ответ, возвращаемый клиенту:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
/**
 * {@link ExceptionMapper} for {@link ValidationException}.
 * <p>
 * Send a {@link ViolationReport} in {@link Response} in addition to HTTP 400/500 status code. Supported media types
 * are: {@code application/json} / {@code application/xml} (if appropriate provider is registered on server).
 * </p>
 *
 * @see org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper The original WildFly class:
 *      {@code org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper}
 */
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
 
    @Override
    public Response toResponse(ValidationException exception) {
        if (exception instanceof ConstraintDefinitionException) {
            return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
        }
        if (exception instanceof ConstraintDeclarationException) {
            return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
        }
        if (exception instanceof GroupDefinitionException) {
            return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
        }
        if (exception instanceof ResteasyViolationException) {
            ResteasyViolationException resteasyViolationException = ResteasyViolationException.class.cast(exception);
            Exception e = resteasyViolationException.getException();
            if (e != null) {
                return buildResponse(unwrapException(e), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
            } else if (resteasyViolationException.getReturnValueViolations().size() == 0) {
                return buildViolationReportResponse(resteasyViolationException, Status.BAD_REQUEST);
            } else {
                return buildViolationReportResponse(resteasyViolationException, Status.INTERNAL_SERVER_ERROR);
            }
        }
        return buildResponse(unwrapException(exception), MediaType.TEXT_PLAIN, Status.INTERNAL_SERVER_ERROR);
    }
 
    protected Response buildResponse(Object entity, String mediaType, Status status) {
        ResponseBuilder builder = Response.status(status).entity(entity);
        builder.type(MediaType.TEXT_PLAIN);
        builder.header(Validation.VALIDATION_HEADER, "true");
        return builder.build();
    }
 
    protected Response buildViolationReportResponse(ResteasyViolationException exception, Status status) {
        ResponseBuilder builder = Response.status(status);
        builder.header(Validation.VALIDATION_HEADER, "true");
 
        // Check standard media types.
        MediaType mediaType = getAcceptMediaType(exception.getAccept());
        if (mediaType != null) {
            builder.type(mediaType);
            builder.entity(new ViolationReport(exception));
            return builder.build();
        }
 
        // Default media type.
        builder.type(MediaType.TEXT_PLAIN);
        builder.entity(exception.toString());
        return builder.build();
    }
 
    protected String unwrapException(Throwable t) {
        StringBuffer sb = new StringBuffer();
        doUnwrapException(sb, t);
        return sb.toString();
    }
 
    private void doUnwrapException(StringBuffer sb, Throwable t) {
        if (t == null) {
            return;
        }
        sb.append(t.toString());
        if (t.getCause() != null && t != t.getCause()) {
            sb.append('[');
            doUnwrapException(sb, t.getCause());
            sb.append(']');
        }
    }
 
    private MediaType getAcceptMediaType(List<MediaType> accept) {
        Iterator<MediaType> it = accept.iterator();
        while (it.hasNext()) {
            MediaType mt = it.next();
            /*
             * application/xml media type causes an exception:
             * org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response
             * object of type: org.jboss.resteasy.api.validation.ViolationReport of media type: application/xml
             */
            /*if (MediaType.APPLICATION_XML_TYPE.getType().equals(mt.getType())
                    && MediaType.APPLICATION_XML_TYPE.getSubtype().equals(mt.getSubtype())) {
                return MediaType.APPLICATION_XML_TYPE;
            }*/
            if (MediaType.APPLICATION_JSON_TYPE.getType().equals(mt.getType())
                    && MediaType.APPLICATION_JSON_TYPE.getSubtype().equals(mt.getSubtype())) {
                return MediaType.APPLICATION_JSON_TYPE;
            }
        }
        return null;
    }
}

Приведенный выше пример является реализацией интерфейса ExceptionMapper который отображает исключения типа ValidationException . Это исключение выдается реализацией Validator при сбое проверки. Если исключение является экземпляром ResteasyViolationException мы отправляем ViolationReport в ответе в дополнение к коду состояния HTTP 400/500. Это гарантирует, что клиент получает отформатированный ответ, а не только исключение, распространяемое из ресурса.

Полученный результат выглядит следующим образом (в формате JSON):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
{
    "exception": null,
    "fieldViolations": [],
    "propertyViolations": [],
    "classViolations": [],
    "parameterViolations": [
        {
            "constraintType": "PARAMETER",
            "path": "getPerson.id",
            "message": "The id must be a valid number",
            "value": "test"
        }
    ],
    "returnValueViolations": []
}

Запуск и тестирование

Чтобы запустить приложение, используемое в этой статье, соберите проект с Maven, разверните его на сервере приложений WildFly 8 и укажите в браузере http: // localhost: 8080 / jaxrs-beanvalidation-javaee7 / .

Кроме того, вы можете запустить тесты из класса PersonsIT которые построены с помощью Arquillian и JUnit . Arquillian автоматически запустит встроенный контейнер WildFly 8, поэтому убедитесь, что у вас нет другого сервера, работающего на тех же портах.

Предложения и улучшения

  1. Мы зависим от кода сервера приложений для реализации собственного провайдера Validator. В GlassFish 4 необходимо реализовать ContextResolver<ValidationConfig> , а в WildFly 8 нам необходимо реализовать ContextResolver<GeneralValidator> . Почему не определен интерфейс в спецификации Java EE 7, который должны реализовывать и ValidationConfig и GeneralValidator вместо того, чтобы полагаться на специфический код сервера приложений?
  2. Сделайте WildFly 8 Embedded проще в использовании и настройке с Maven. В настоящее время, чтобы он был доступен для Arquillian, необходимо скачать дистрибутив WildFly (org.wildfly: wildfly-dist), разархивировать его в target папку и настроить системные свойства в плагинах Surefire / Failsafe Maven:
    1
    2
    3
    4
    5
    <systemPropertyVariables>
        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
        <jboss.home>${wildfly.home}</jboss.home>
        <module.path>${wildfly.home}/modules</module.path>
    </systemPropertyVariables>

    Принимая во внимание, что для Glassfish вам просто нужно определить правильную зависимость (org.glassfish.main.extras: glassfish-embedded-all).

  3. Сделайте RESTEasy транзитивной зависимостью от WildFly Embedded. Наличие всех модулей WildFly, доступных во время компиляции, только путем определения provided зависимости WildFly Embedded, было бы хорошим производительным стимулом.
  4. В настоящее время невозможно использовать опцию Run As >> JUnit Test в Eclipse, поскольку должно существовать системное свойство с именем jbossHome . Это свойство не читается из конфигурации Surefire / Failsafe Eclipse. Есть ли обходной путь для этого?
  5. При использовании RESTEasy по умолчанию реализация ExceptionMapper<ValidationException> , запрашивающая данные в типе носителя application/xml и имеющая ошибки проверки, выдаст следующее исключение:
    1
    2
    3
    4
    org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure:
        Could not find MessageBodyWriter for response object of type:
            org.jboss.resteasy.api.validation.ViolationReport of media type:
                application/xml

    Это ошибка RESTEasy?