Я искал, как управлять версиями REST API с помощью Spring 3.2.x, но не нашел ничего, что было бы легко поддерживать. Сначала я объясню свою проблему, а затем решение ... но мне интересно, изобретаю ли я здесь колесо заново.
Я хочу управлять версией на основе заголовка Accept, и, например, если запрос имеет заголовок Accept application/vnd.company.app-1.1+json
, я хочу, чтобы Spring MVC перенаправлял его методу, который обрабатывает эту версию. И поскольку не все методы в API меняются в одном и том же выпуске, я не хочу переходить к каждому из моих контроллеров и изменять что-либо для обработчика, которое не изменилось между версиями. Я также не хочу иметь логику, чтобы выяснить, какую версию использовать в самом контроллере (с помощью локаторов служб), поскольку Spring уже обнаруживает, какой метод вызывать.
Итак, взяв API с версиями 1.0 до 1.8, где обработчик был представлен в версии 1.0 и модифицирован в версии 1.7, я бы хотел обработать это следующим образом. Представьте, что код находится внутри контроллера, и что есть код, который может извлечь версию из заголовка. (Следующее недействительно весной)
@RequestMapping(...)
@VersionRange(1.0,1.6)
@ResponseBody
public Object method1() {
// so something
return object;
}
@RequestMapping(...) //same Request mapping annotation
@VersionRange(1.7)
@ResponseBody
public Object method2() {
// so something
return object;
}
Это невозможно весной, так как два метода имеют одинаковую RequestMapping
аннотацию и Spring не загружается. Идея состоит в том, что VersionRange
аннотация может определять диапазон открытой или закрытой версии. Первый метод действителен для версий с 1.0 по 1.6, а второй - для версии 1.7 и выше (включая последнюю версию 1.8). Я знаю, что этот подход ломается, если кто-то решает передать версию 99.99, но я могу с этим жить.
Теперь, поскольку вышеизложенное невозможно без серьезной доработки того, как работает Spring, я думал о том, чтобы поработать с тем, как обработчики сопоставляются с запросами, в частности, чтобы написать свой собственный ProducesRequestCondition
, и иметь там диапазон версий. Например
Код:
@RequestMapping(..., produces = "application/vnd.company.app-[1.0-1.6]+json)
@ResponseBody
public Object method1() {
// so something
return object;
}
@RequestMapping(..., produces = "application/vnd.company.app-[1.7-]+json)
@ResponseBody
public Object method2() {
// so something
return object;
}
Таким образом, я могу определить диапазоны закрытых или открытых версий в части аннотации, которая производит. Я работаю над этим решением сейчас, с проблемой, что мне все еще пришлось заменить некоторые основные классы Spring MVC ( RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
и RequestMappingInfo
), что мне не нравится, потому что это означает дополнительную работу всякий раз, когда я решаю перейти на более новую версию весна.
Я был бы признателен за любые мысли ... и особенно любое предложение сделать это более простым и легким в обслуживании способом.
редактировать
Добавление награды. Чтобы получить награду, ответьте на вопрос выше, не предлагая использовать эту логику в самом контроллере. В Spring уже есть много логики для выбора вызываемого метода контроллера, и я хочу воспользоваться этим.
Редактировать 2
Я поделился исходным POC (с некоторыми улучшениями) в github: https://github.com/augusto/restVersioning
источник
produces={"application/json-1.0", "application/json-1.1"}
etcОтветы:
Независимо от того, можно ли избежать управления версиями, выполнив обратно совместимые изменения (что не всегда возможно, если вы связаны некоторыми корпоративными правилами или ваши клиенты API реализованы с ошибками и будут нарушать, даже если они не должны), абстрактное требование является интересным один:
Как я могу сделать настраиваемое сопоставление запроса, которое выполняет произвольные оценки значений заголовка из запроса без выполнения оценки в теле метода?
Как описано в этом ответе SO, вы на самом деле можете иметь то же самое
@RequestMapping
и использовать другую аннотацию, чтобы различать во время фактической маршрутизации, которая происходит во время выполнения. Для этого вам необходимо:VersionRange
.RequestCondition<VersionRange>
. Поскольку у вас будет что-то вроде алгоритма наилучшего соответствия, вам нужно будет проверить, обеспечивают ли методы, аннотированные другимиVersionRange
значениями, лучшее соответствие текущему запросу.VersionRangeRequestMappingHandlerMapping
основе аннотации и условия запроса (как описано в сообщении Как реализовать настраиваемые свойства @RequestMapping ).VersionRangeRequestMappingHandlerMapping
перед использованием значения по умолчаниюRequestMappingHandlerMapping
(например, установив его порядок на 0).Это не потребует каких-либо хакерских замен компонентов Spring, но использует механизмы конфигурации и расширения Spring, поэтому он должен работать, даже если вы обновляете свою версию Spring (если новая версия поддерживает эти механизмы).
источник
mvc:annotation-driven
. Надеюсь, Spring предоставит версию,mvc:annotation-driven
в которой можно определять собственные условия.Я только что создал индивидуальное решение. Я использую
@ApiVersion
аннотацию в сочетании с@RequestMapping
аннотацией внутри@Controller
классов.Пример:
Реализация:
Аннотация ApiVersion.java :
ApiVersionRequestMappingHandlerMapping.java (в основном это копирование и вставка из
RequestMappingHandlerMapping
):Инъекция в WebMvcConfigurationSupport:
источник
/v1/aResource
и/v2/aResource
выглядят как разные ресурсы, но это просто другое представление одного и того же ресурса! 2. Использование заголовков HTTP выглядит лучше, но вы не можете дать кому-либо URL-адрес, потому что URL-адрес не содержит заголовка. 3. Использование параметра URL, то есть/aResource?v=2.1
(кстати: так Google делает версии)....
Я все еще не уверен, что выберу вариант 2 или 3 , но я больше никогда не буду использовать 1 по причинам, указанным выше.RequestMappingHandlerMapping
в свойWebMvcConfiguration
, вы должны перезаписатьcreateRequestMappingHandlerMapping
вместоrequestMappingHandlerMapping
! В противном случае вы столкнетесь со странными проблемами (у меня внезапно возникли проблемы с ленивой инициализацией Hibernates из-за закрытого сеанса)WebMvcConfigurationSupport
а расширятьDelegatingWebMvcConfiguration
. Это работает для меня (см stackoverflow.com/questions/22267191/... )Я бы по-прежнему рекомендовал использовать URL-адреса для управления версиями, потому что в URL-адресах @RequestMapping поддерживает шаблоны и параметры пути, формат которых можно указать с помощью regexp.
А для обработки обновлений клиента (которые вы упомянули в комментарии) вы можете использовать псевдонимы, такие как «последний». Или у вас есть неверсированная версия api, которая использует последнюю версию (да).
Также, используя параметры пути, вы можете реализовать любую сложную логику обработки версий, и если вы уже хотите иметь диапазоны, вы вполне можете в ближайшее время захотеть чего-то большего.
Вот пара примеров:
На основе последнего подхода вы можете реализовать что-то вроде того, что хотите.
Например, у вас может быть контроллер, содержащий только методы с обработкой версий.
В этой обработке вы смотрите (используя библиотеки отражения / AOP / генерации кода) в каком-то весеннем сервисе / компоненте или в том же классе для метода с тем же именем / подписью и требуемым @VersionRange и вызываете его, передавая все параметры.
источник
Я реализовал решение, которое ИДЕАЛЬНО решает проблему с оставшимися версиями.
Общие положения Есть 3 основных подхода к управлению версиями в остальном:
Подход на основе пути , при котором клиент определяет версию в URL:
Заголовок Content-Type , в котором клиент определяет версию в заголовке Accept :
Пользовательский заголовок , в котором клиент определяет версию в настраиваемом заголовке.
Проблема с первым подходом заключается в том, что если вы измените версию , скажем , от v1 -> v2, вероятно , вам нужно скопировать и вставить в v1 ресурсах, которые не изменились на путь v2
Проблема с вторым подходом заключается в том, что некоторые инструментах , такие как http://swagger.io/ не может различить между операциями с таким же путем , но разными Content-Type (проверка выпуск https://github.com/OAI/OpenAPI-Specification/issues/ 146 )
Решение
Поскольку я много работаю с инструментами остальной документации, я предпочитаю использовать первый подход. Мое решение решает проблему с первым подходом, поэтому вам не нужно копировать и вставлять конечную точку в новую версию.
Допустим, у нас есть версии v1 и v2 для контроллера User:
Требование , если я просить v1 для ресурса пользователя я должен принять «V1» Пользователь repsonse, в противном случае , если я просить v2 , v3 и так далее я должен принять «V2 пользователя» ответ.
Чтобы реализовать это весной, нам нужно переопределить поведение RequestMappingHandlerMapping по умолчанию :
}
Реализация считывает версию в URL-адресе и просит весной разрешить URL-адрес. Если этот URL-адрес не существует (например, клиент запросил v3 ), мы пробуем с v2 и так далее, пока не найдем самую последнюю версию для ресурса. ,
Чтобы увидеть преимущества этой реализации, скажем, у нас есть два ресурса: Пользователь и Компания:
Допустим, мы внесли изменение в «контракт» компании, которое разрывает клиента. Поэтому мы реализуем
http://localhost:9001/api/v2/company
и просим клиента перейти на v2 вместо v1.Итак, новые запросы от клиента:
вместо того:
Самое приятное здесь то, что с этим решением клиент будет получать информацию о пользователе из v1 и информацию о компании из v2 без необходимости создавать новую (такую же) конечную точку от пользователя v2!
Остальная документация Как я уже сказал, причина, по которой я выбираю подход к управлению версиями на основе URL, заключается в том, что некоторые инструменты, такие как swagger, не документируют по-разному конечные точки с одним и тем же URL, но с другим типом контента. В этом решении отображаются обе конечные точки, поскольку имеют разные URL-адреса:
GIT
Реализация решения по адресу: https://github.com/mspapant/restVersioningExample/
источник
@RequestMapping
Аннотацию поддерживаетheaders
элемент , который позволяет сузить запросы соответствия. В частности, вы можете использоватьAccept
здесь заголовок.Это не совсем то, что вы описываете, поскольку он не обрабатывает диапазоны напрямую, но элемент поддерживает подстановочный знак *, а также! =. Так что, по крайней мере, вы могли бы избежать использования подстановочного знака для случаев, когда все версии поддерживают рассматриваемую конечную точку или даже все второстепенные версии данной основной версии (например, 1. *).
Не думаю, что я действительно использовал этот элемент раньше (если и использовал, не помню), поэтому я просто ухожу от документации на
http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html
источник
application/*
а не с его частями. Например, в Spring недопустимо следующее"Accept=application/vnd.company.app-1.*+json"
. Это связано как класс весеннихMediaType
работА как насчет простого использования наследования для управления версиями? Это то, что я использую в своем проекте, и он не требует специальной настройки пружины и дает мне именно то, что я хочу.
Такая настройка позволяет с минимальным дублированием кода и возможностью перезаписывать методы в новые версии API с минимальными усилиями. Это также избавляет от необходимости усложнять исходный код с помощью логики переключения версий. Если вы не закодируете конечную точку в версии, она по умолчанию захватит предыдущую версию.
По сравнению с тем, что делают другие, это кажется намного проще. Что-то мне не хватает?
источник
Я уже пробовал версию своего API, используя управление версиями URI , например:
Но при попытке заставить эту работу работать возникают некоторые проблемы: как организовать свой код с разными версиями? Как управлять двумя (или более) версиями одновременно? Какое влияние при удалении какой-то версии?
Лучшей альтернативой, которую я нашел, была не версия всего API, а управление версией на каждой конечной точке . Этот шаблон называется управлением версиями с использованием заголовка Accept или управлением версиями через согласование содержимого :
Реализация на Spring
Сначала вы создаете контроллер с базовым атрибутом productions, который будет применяться по умолчанию для каждой конечной точки внутри класса.
После этого создайте возможный сценарий, в котором у вас есть две версии конечной точки для создания заказа:
Готово! Просто вызовите каждую конечную точку, используя нужную версию заголовка Http :
Или, чтобы назвать версию два:
О ваших заботах:
Как объяснялось, эта стратегия поддерживает каждый контроллер и конечную точку с его фактической версией. Вы изменяете только ту конечную точку, которая имеет модификации и нуждается в новой версии.
А развязность?
С помощью этой стратегии также очень легко настроить Swagger с разными версиями. См. Этот ответ для получения более подробной информации.
источник
В производстве вы можете иметь отрицание. Итак, для метода 1 говорят,
produces="!...1.7"
а для метода 2 положительные.Производит также массив, поэтому вы для метода 1 можете сказать и
produces={"...1.6","!...1.7","...1.8"}
т.д. (принять все, кроме 1.7)Конечно, не так идеально, как диапазоны, которые вы имеете в виду, но я думаю, что легче поддерживать, чем другие настраиваемые вещи, если это что-то необычное в вашей системе. Удачи!
источник
Вы можете использовать АОП вокруг перехвата
Рассмотрите возможность отображения запроса, который получает все
/**/public_api/*
и в этом методе ничего не делает;После
Единственное ограничение - все должно находиться в одном контроллере.
Для конфигурации AOP посмотрите http://www.mkyong.com/spring/spring-aop-examples-advice/
источник