Простое управление ресурсами REST в реализациях на основе JAX-RS?

Лучшей практикой для управления версиями ресурсов 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 должна быть реализована для поддержки этого.

Вы можете что-то предложить?

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

Ответ 1

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

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

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

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

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

Accept: application/vnd.COMPANY.systeminfo+json

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

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

Затем метод THAT берет "внеполосную" информацию о версии для обработки деталей при обработке (например, для выбора подходящего класса для загрузки через 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. Единственная другая проблема заключается в том, что ваши классы, если они будут выглядеть, будут видеть измененный Путь, а не исходный путь (но ваш фильтр может заполнить это в запросе в качестве ссылки, если хотите).

Он не идеален, но он (в основном) свободен.

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

Ответ 2

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

Content-Type: Приложение /vnd.COMPANY.systeminfo- {версия} + JSON

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

Ответ 3

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


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