Простое управление версиями ресурса REST в реализациях на основе JAX-RS? - PullRequest
11 голосов
/ 07 февраля 2011

Лучшая практика для управления версиями ресурса REST - размещение информации о версии в заголовках Accept / Content-Type HTTP-запроса, оставляя URI без изменений.

Вот пример запроса / ответа к REST API для получения системной информации:

==>
GET /api/system-info HTTP/1.1
Accept: application/vnd.COMPANY.systeminfo-v1+json

<==
HTTP/1.1 200 OK
Content-Type: application/vnd.COMPANY.systeminfo-v1+json
{
  “session-count”: 19
}

Обратите внимание, что версия указана в MIME-типе.

Вот еще один запрос / ответ для версии 2:

==>
GET /api/system-info HTTP/1.1
Accept: application/vnd.COMPANY.systeminfo-v2+json

<==
HTTP/1.1 200 OK
Content-Type: application/vnd.COMPANY.systeminfo-v2+json
{
  “uptime”: 234564300,
  “session-count”: 19
}

См. http://barelyenough.org/blog/tag/rest-versioning/ для более подробного объяснения и примеров.

Можно ли легко реализовать этот подход в реализациях на основе Java JAX-RS, таких как Jersey или Apache CXF?

Цель состоит в том, чтобы иметь несколько классов @Resource с одинаковым значением @Path, но обслуживающих запрос на основе фактической версии, указанной в типе MIME?

Я изучал JAX-RS в целом и Джерси в частности и не нашел поддержки для этого. Джерси не дает возможности зарегистрировать два ресурса с одинаковым путем. Для поддержки этого необходимо реализовать замену для класса WebApplicationImpl.

Можете ли вы что-нибудь предложить?

ПРИМЕЧАНИЕ. Требуется, чтобы несколько версий одного и того же ресурса были доступны одновременно. Новые версии могут вносить несовместимые изменения.

Ответы [ 4 ]

6 голосов
/ 08 февраля 2011

JAX-RS отправляет методы, помеченные @Produces, через заголовок Accept. Итак, если вы хотите, чтобы JAX-RS осуществлял диспетчеризацию, вам нужно использовать этот механизм. Без дополнительной работы вам придется создать метод (и поставщика) для каждого типа носителя, который вы хотите поддерживать.

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

Одна из идей - добавить фильтр, который «нормализует» заголовок Accept специально для отправки. То есть, возможно, вы берете:

Accept: application/vnd.COMPANY.systeminfo-v1+json

И, преобразовав это, просто:

Accept: application/vnd.COMPANY.systeminfo+json

В то же время вы извлекаете информацию о версии для последующего использования (возможно, в запросе или каком-либо другом специальном механизме).

Затем JAX-RS отправит один метод, который обрабатывает "application / vnd.COMPANY.systeminfo + json".

Затем этот метод получает информацию о версиях «out-of-band» для обработки деталей при обработке (например, выбор подходящего класса для загрузки через OSGi).

Затем вы создаете провайдера с соответствующим MessageBodyWriter. JAX-RS выберет поставщика для типа носителя application / vnd.COMPANY.systeminfo + json. От вашего MBW будет зависеть выяснение фактического типа носителя (опять же на основе этой информации о версии) и создание правильного выходного формата (опять же, возможно, отправка в правильный загруженный класс OSGi).

Я не знаю, может ли MBW перезаписать заголовок Content-Type или нет. Если нет, то вы можете делегировать более ранний фильтр, чтобы переписать эту часть для вас на выходе.

Это немного запутанно, но если вы хотите использовать диспетчеризацию JAX-RS, а не создавать методы для каждой версии вашего типа носителя, тогда это возможный путь для этого.

Редактировать в ответ на комментарий:

Да, по сути, вы хотите, чтобы JAX-RS отправлял в соответствующий класс на основе типа Path и Accept. Маловероятно, что JAX-RS сделает это «из коробки», так как это немного необычный случай. Я не смотрел ни на одну из реализаций JAX-RS, но вы можете сделать то, что вы хотите, настроив одну из них на уровне инфраструктуры.

Возможно, другой менее инвазивный вариант - использовать старый трюк из мира Apache и просто создать фильтр, который переписывает ваш путь на основе заголовка Accept.

Итак, когда система получает:

GET /resource
Accept: application/vnd.COMPANY.systeminfo-v1+json

Вы переписываете его на:

GET /resource-v1
Accept: application/vnd.COMPANY.systeminfo-v1+json

Затем в вашем классе JAX-RS:

@Path("resource-v1")
@Produces("application/vnd.COMPANY.systeminfo-v1+json")
public class ResourceV1 {
    ...
}

Итак, ваши клиенты получают правильное представление, но JAX-RS правильно отправляет ваши классы. Единственная другая проблема заключается в том, что ваши классы, если они посмотрят, увидят измененный путь, а не исходный путь (но ваш фильтр может добавить это в запрос в качестве ссылки, если хотите).

Это не идеально, но это (в основном) бесплатно.

Этот - это существующий фильтр, который может делать то, что вы хотите сделать, если нет, то он может послужить вам вдохновением, чтобы сделать это самостоятельно.

1 голос
/ 18 июня 2018

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

Пример:

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;

@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public VersionOneDTO get(@PathParam("id") final String id) {

    return new VersionOneDTO( ... );

}

@GET
@Path("/{id}")
@Produces("application/vnd.COMPANY.systeminfo-v2+json;qs=0.9")
public VersionTwoDTO get_v2(@PathParam("id") final String id) {

    return new VersionTwoDTO( ... );

}

Если методы get(...) и get_v2(...) используют общую логику, я бы предложил поместить ее в общий частный метод, если он связан с API (например, обработка сеанса или JWT), или в общий публичный метод уровня обслуживания. что вы получаете доступ через наследование или инъекцию зависимостей. Имея два разных метода с разными типами возвращаемых данных, вы гарантируете, что возвращаемая структура имеет правильный тип для разных версий API.

Обратите внимание, что некоторые старые клиенты могут вообще не указывать заголовок Accept. Это неявно означает, что они будут принимать любой тип контента, то есть любую версию вашего API. На практике это чаще всего не правда. По этой причине вам следует указать вес для более новых версий API, используя расширение qs типа MIME, как показано в аннотации @Produces в приведенном выше примере.

Если вы тестируете с restAssured, это будет выглядеть примерно так:

import static com.jayway.restassured.RestAssured.get;
import static com.jayway.restassured.RestAssured.given;

@Test
public void testGetEntityV1() {
    given()
        .header("Accept", MediaType.APPLICATION_JSON)
    .when()
        .get("/basepath/1")
    .then()
        .assertThat()
        ... // Some check that Version 1 was called
    ;
}

@Test
public void testGetEntityV1OldClientNoAcceptHeader() {
    get("/basepath/1")
        .then()
        .assertThat()
        ... // Some check that Version 1 was called
    ;
}

@Test
public void testGetEntityV2() {
    given()
        .header("Accept", "application/vnd.COMPANY.systeminfo-v2+json")
    .when()
        .get("/basepath/1")
    .then()
        .assertThat()
        ... // Some check that Version 2 was called
    ;
}
0 голосов
/ 08 февраля 2011

Если вы используете CXF, вы можете использовать методику, указанную здесь , чтобы создать нового поставщика сериализации (построение на основе существующей инфраструктуры), который производит данные в определенном желаемом формате. Объявите несколько из них, по одному для каждого конкретного формата, который вы хотите, и используйте аннотацию @Produces, чтобы механизм мог обработать остальную часть согласования для вас, хотя также может быть также идея поддержки стандартного типа содержимого JSON. так что нормальные клиенты могут справиться с этим без необходимости уклоняться от вашей специализации. Тогда единственным реальным вопросом становится, каков наилучший способ сериализации; Я полагаю, вы сами можете это понять ...


[РЕДАКТИРОВАТЬ]: Дальнейшее изучение документации CXF приводит к открытию, что аннотации @Consumes и @Produces считаются осями для выбора. Если вы хотите иметь два метода, которые обрабатывают ответ для разных типов носителей, вы, безусловно, можете. (Вам придется добавить поставщиков сериализации и / или десериализации, если вы используете пользовательские типы, но вы можете выполнять делегирование большей части работы стандартным поставщикам.) Я все же хотел бы предупредить, что вам следует по-прежнему убедитесь, что ресурс, указанный путем, должен быть одинаковым в обоих случаях; делать иначе - не RESTful.

0 голосов
/ 07 февраля 2011

Одним из возможных решений является использование одного @Path с

Content-Type: application / vnd.COMPANY.systeminfo- {версия} + json

Затем,внутри метода данного @Path вы можете вызвать версию WebService

...