Статьи

Ваши JAX-RS API-интерфейсы не были одинаковыми: использование динамических функций

На этот раз мы немного поговорим об API JAX-RS 2.0 и коснемся одного очень интересного аспекта спецификации: динамических функций и их полезности.

Традиционно, когда API -интерфейсы JAX-RS 2.0 конфигурируются и развертываются (с использованием класса Application , загружаемого из сервлета или создаваемого с помощью RuntimeDelegate ), существует возможность зарегистрировать дополнительных поставщиков и функции . Прекрасными примерами этого могут быть проверка бина (JSR 349) или поддержка Java API для обработки JSON (JSR-353) . Эти провайдеры и функции будут применяться ко всем ресурсам JAX-RS 2.0, и в большинстве случаев это желаемое поведение. Однако время от времени возникает необходимость включить определенного поставщика или функцию только для некоторых ресурсов, оставляя другие незатронутыми. Это как раз тот случай, когда нам очень помогут динамические функции .

В этом посте мы собираемся использовать последнюю версию 3.1.5 превосходной платформы Apache CXF, но динамические функции являются частью спецификации JAX-RS 2.0 и поддерживаются большинством (если не всеми) реализациями.

Давайте рассмотрим очень простой JAX-RS 2.0 API для управления людьми с единственным методом для обработки запросов HTTP GET . Предположим, что это версия 1 API, и хотя аннотация @Range указана для параметра запроса count , его поддержка никогда не реализовывалась и присутствует в коде только для целей документирования.

1
2
3
4
5
6
7
8
@Path("/v1/people")
public class PeopleRestService {
    @Produces( { MediaType.APPLICATION_JSON } )
    @GET
    public List<Person> getAll(@Range(min = 1, max = 10) @QueryParam("count") int count) {
        return Collections.nCopies(count, new Person("[email protected]", "A", "B"));
    }
}

В этом случае передача недопустимого значения для параметра запроса количества приведет к внутренней ошибке сервера . Давайте удостоверимся, что это именно то, что происходит:

1
2
3
4
5
6
7
8
$ curl -i http://localhost:8080/rest/api/v1/people?count=-1
 
HTTP/1.1 500 Server Error
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1
Content-Length: 377
Connection: close
Server: Jetty(9.3.7.v20160115)

Через некоторое время мы осознали проблемы с этим API и решили внедрить надлежащий механизм проверки на месте, используя интеграцию Bean Validation 1.1 с JAX-RS 2.0 . Однако мы приняли решение создать API версии 2 и оставить версию 1 без изменений, поскольку ее клиенты не ожидают возврата каких-либо других кодов состояния HTTP, кроме 200 и 500 (к сожалению, в реальной жизни это случается чаще, чем нет). ,

Существует несколько различных подходов для реализации такой настройки для каждого API, но, вероятно, самый простой из них — это введение отдельной аннотации, например @EnableBeanValidation , и аннотирование класса ресурсов JAX-RS 2.0 вместе с ним:

1
2
3
4
5
6
7
8
9
@Path("/v2/people")
@EnableBeanValidation
public class ValidatingPeopleRestService {
    @Produces( { MediaType.APPLICATION_JSON } )
    @GET
    public @Valid List<Person> getAll(@Range(min = 1, max = 10) @QueryParam("count") int count) {
        return Collections.nCopies(count, new Person("[email protected]", "A", "B"));
    }
}

Чтобы включить Bean Validation 1.1 для всех API -интерфейсов JAX-RS 2.0, аннотированных @EnableBeanValidation, мы собираемся создать динамический класс пространственных объектов , BeanValidationDynamicFeature :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Provider
public class BeanValidationDynamicFeature implements DynamicFeature {
    private final JAXRSBeanValidationInInterceptor inInterceptor;
    private final JAXRSBeanValidationOutInterceptor outInterceptor;
     
    public BeanValidationDynamicFeature(final BeanValidationProvider provider) {
        this.inInterceptor = new JAXRSBeanValidationInInterceptor();
        this.inInterceptor.setProvider(provider);
         
        this.outInterceptor = new JAXRSBeanValidationOutInterceptor();
        this.outInterceptor.setProvider(provider);
    }
     
    @Override
    public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
        if (resourceInfo.getResourceClass().getAnnotation(EnableBeanValidation.class) != null) {
            context.register(inInterceptor);
            context.register(outInterceptor);
        }
    }
}

Его работа довольно проста, просто зарегистрируйте JAXRSBeanValidationInInterceptor и JAXRSBeanValidationOutInterceptor экземпляры-перехватчики в качестве дополнительных провайдеров для рассматриваемых API JAX-RS 2.0 . Одно небольшое, но важное замечание: средства отображения исключений не поддерживаются динамическими функциями , по крайней мере, в отношении реализации Apache CXF , и должны быть зарегистрированы как обычные поставщики (вместе с самими динамическими функциями ), например:

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
@Bean @DependsOn("cxf")
public Server jaxRsServer() {
    final JAXRSServerFactoryBean factory =
        RuntimeDelegate.getInstance().createEndpoint(
            jaxRsApiApplication(),
            JAXRSServerFactoryBean.class
        );
         
    factory.setServiceBean(validatingPeopleRestService());
    factory.setServiceBean(peopleRestService());
    factory.setProvider(new JacksonJsonProvider());
    factory.setProvider(new BeanValidationDynamicFeature(new BeanValidationProvider()));
    factory.setProvider(new ValidationExceptionMapper());
         
    return factory.create();
}
 
@Bean
public JaxRsApiApplication jaxRsApiApplication() {
    return new JaxRsApiApplication();
}
     
@Bean
public ValidatingPeopleRestService validatingPeopleRestService() {
    return new ValidatingPeopleRestService();
}
     
@Bean
public PeopleRestService peopleRestService() {
    return new PeopleRestService();
}

Это в основном все, что мы должны сделать. Как только BeanValidationDynamicFeature зарегистрирован (в этом случае, используя JAXRSServerFactoryBean ), он будет применен ко всем соответствующим служебным компонентам. Давайте удостоверимся, что для версии 2 нашего API управления персоналом запускается правильная готовая проверка:

1
2
3
4
5
$ curl -i http://localhost:8080/rest/api/v2/people?count=-1
 
HTTP/1.1 400 Bad Request
Content-Length: 0
Server: Jetty(9.3.7.v20160115)

На этот раз ответ отличается, что указывает на то, что клиент отправил неверный ввод (прямой результат проверки Bean 1.1 в действии): Bad Request .

Надеемся, что динамические функции станут еще одним полезным инструментом в вашем наборе инструментов. Пример, который мы рассмотрели здесь, несколько воображаем, но очень просто использовать динамические функции с безопасностью, трассировкой, ведением журналов, профилированием и т. Д. Кроме того, динамические функции можно применять даже к определенным методам ресурсов, что позволяет осуществлять точный контроль над вашими API. ,

  • Полный исходный код проекта доступен на Github .