Как управлять версиями REST API с помощью spring?
Я искал, как управлять версиями 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 и изменен в v1.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;
}
это невозможно весной, так как 2 метода имеют то же самое RequestMapping
аннотация и пружина не загружаются. Идея в том, что VersionRange
аннотация может определять открытый или закрытый диапазон версий. Первый метод действителен от версий 1.0 до 1.6, в то время как второй для версии 1.7 и далее (включая последняя версия 1.8). Я знаю, что этот подход ломается, если кто-то решает пройти версию 99.99, но это то, с чем я могу жить.
теперь, поскольку вышесказанное невозможно без серьезной переделки того, как работает весна, я думал о том, чтобы возиться с тем, как обработчики соответствуют запросам, в частности, написать свой собственный 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.
Я был бы признателен за любые мысли... и особенно, любое предложение сделать это проще,проще в обслуживании.
редактировать
добавить щедрость. Чтобы получить награду, пожалуйста, ответьте на вопрос выше, не предлагая иметь эту логику в самом контроллере. Spring уже имеет много логики для выбора метода контроллера для вызова, и я хочу вернуться к этому.
Изменить 2
я поделился оригинальным POC (с некоторыми улучшениями) в github:https://github.com/augusto/restVersioning
8 ответов
независимо от того, можно ли избежать управления версиями, выполнив обратно совместимые изменения (что не всегда возможно, когда вы связаны некоторыми корпоративными рекомендациями или ваши клиенты API реализованы багги и сломаются, даже если они не должны), абстрактное требование является интересным:
как я могу сделать пользовательское сопоставление запроса, которое выполняет произвольные оценки значений заголовка из запроса без выполнения оценки в методе тело?
как описано в это так ответ вы на самом деле можете иметь то же самое @RequestMapping
и используйте другую аннотацию, чтобы различать во время фактической маршрутизации, которая происходит во время выполнения. Для этого вам придется:
- создать новую аннотации
VersionRange
. - реализовать
RequestCondition<VersionRange>
. Поскольку у вас будет что-то вроде алгоритма наилучшего соответствия, вам придется проверить, аннотированы ли методы с другимиVersionRange
значения обеспечивают лучше соответствовать текущему запросу. - реализовать
VersionRangeRequestMappingHandlerMapping
на основе аннотации и условия запроса (как описано в сообщении как реализовать пользовательские свойства @RequestMapping ). - настройка spring для оценки вашего
VersionRangeRequestMappingHandlerMapping
перед использованием по умолчаниюRequestMappingHandlerMapping
(например, установив его порядок до 0).
это не потребует каких-либо хакерских замен компонентов Spring, но использует конфигурацию и расширение Spring механизмы, поэтому он должен работать, даже если вы обновляете свою весеннюю версию (если новая версия поддерживает эти механизмы).
Я только что создал пользовательское решение. Я использую @ApiVersion
аннотация в сочетании с @RequestMapping
аннотация внутри @Controller
классы.
пример:
@Controller
@RequestMapping("x")
@ApiVersion(1)
class MyController {
@RequestMapping("a")
void a() {} // maps to /v1/x/a
@RequestMapping("b")
@ApiVersion(2)
void b() {} // maps to /v2/x/b
@RequestMapping("c")
@ApiVersion({1,3})
void c() {} // maps to /v1/x/c
// and to /v3/x/c
}
реализация:
Апиверсия.java аннотация:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
int[] value();
}
ApiVersionRequestMappingHandlerMapping.java (это в основном копирование и вставка из RequestMappingHandlerMapping
):
public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private final String prefix;
public ApiVersionRequestMappingHandlerMapping(String prefix) {
this.prefix = prefix;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
if(info == null) return null;
ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
if(methodAnnotation != null) {
RequestCondition<?> methodCondition = getCustomMethodCondition(method);
// Concatenate our ApiVersion with the usual request mapping
info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
} else {
ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
if(typeAnnotation != null) {
RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
// Concatenate our ApiVersion with the usual request mapping
info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
}
}
return info;
}
private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
int[] values = annotation.value();
String[] patterns = new String[values.length];
for(int i=0; i<values.length; i++) {
// Build the URL prefix
patterns[i] = prefix+values[i];
}
return new RequestMappingInfo(
new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
new RequestMethodsRequestCondition(),
new ParamsRequestCondition(),
new HeadersRequestCondition(),
new ConsumesRequestCondition(),
new ProducesRequestCondition(),
customCondition);
}
}
инъекции в WebMvcConfigurationSupport:
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping("v");
}
}
Я бы все равно рекомендовал использовать URL для управления версиями, потому что в URLs @RequestMapping поддерживает шаблоны и параметры пути, формат которых может быть указан с помощью regexp.
и для обработки обновлений клиентов (которые вы упомянули в комментарии) Вы можете использовать псевдонимы, такие как "последние". Или имеют неверсионную версию api, которая использует последнюю версию (да).
также используя параметры пути, вы можете реализовать любую сложную логику обработки версий, и если вы уже хотите иметь диапазоны, очень скоро тебе захочется чего-то большего.
вот несколько примеров:
@RequestMapping({
"/**/public_api/1.1/method",
"/**/public_api/1.2/method",
})
public void method1(){
}
@RequestMapping({
"/**/public_api/1.3/method"
"/**/public_api/latest/method"
"/**/public_api/method"
})
public void method2(){
}
@RequestMapping({
"/**/public_api/1.4/method"
"/**/public_api/beta/method"
})
public void method2(){
}
//handles all 1.* requests
@RequestMapping({
"/**/public_api/{version:1\.\d+}/method"
})
public void methodManual1(@PathVariable("version") String version){
}
//handles 1.0-1.6 range, but somewhat ugly
@RequestMapping({
"/**/public_api/{version:1\.[0123456]?}/method"
})
public void methodManual1(@PathVariable("version") String version){
}
//fully manual version handling
@RequestMapping({
"/**/public_api/{version}/method"
})
public void methodManual2(@PathVariable("version") String version){
int[] versionParts = getVersionParts(version);
//manual handling of versions
}
public int[] getVersionParts(String version){
try{
String[] versionParts = version.split("\.");
int[] result = new int[versionParts.length];
for(int i=0;i<versionParts.length;i++){
result[i] = Integer.parseInt(versionParts[i]);
}
return result;
}catch (Exception ex) {
return null;
}
}
на основе последнего подхода вы можете реализовать что-то вроде того, что вы хотите.
например, вы можете иметь контроллер, который содержит только метод stabs с обработкой версии.
в этой обработке вы смотрите (используя библиотеки генерации отражения/AOP/кода) в некотором весеннем сервисе / компоненте или в том же классе для метода с тем же именем/подписью и требуемым @VersionRange и вызвать его, передавая все параметры.
я реализовал решение, которое обрабатывает отлично проблема с управлением версиями rest.
вообще говоря, есть 3 основных подхода для управления версиями rest:
-
путь-основанный approch, в котором клиент определяет версию в URL:
http://localhost:9001/api/v1/user http://localhost:9001/api/v2/user
-
Контент-Тип заголовок, в котором клиент определяет версию в принимать заголовок:
http://localhost:9001/api/v1/user with Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
Заголовок, в котором клиент определяет версию в пользовательский заголовок.
The
на @RequestMapping
аннотация поддерживает headers
элемент, который позволяет сократить соответствующие запросы. В частности, вы можете использовать
Как насчет использования наследования для моделирования управления версиями? Это то, что я использую в своем проекте, и он не требует специальной весенней конфигурации и получает именно то, что я хочу.
@RestController
@RequestMapping(value = "/test/1")
@Deprecated
public class Test1 {
...Fields Getters Setters...
@RequestMapping(method = RequestMethod.GET)
@Deprecated
public Test getTest(Long id) {
return serviceClass.getTestById(id);
}
@RequestMapping(method = RequestMethod.PUT)
public Test getTest(Test test) {
return serviceClass.updateTest(test);
}
}
@RestController
@RequestMapping(value = "/test/2")
public class Test2 extends Test1 {
...Fields Getters Setters...
@Override
@RequestMapping(method = RequestMethod.GET)
public Test getTest(Long id) {
return serviceClass.getAUpdated(id);
}
@RequestMapping(method = RequestMethod.DELETE)
public Test deleteTest(Long id) {
return serviceClass.deleteTestById(id);
}
}
Эта настройка позволяет мало дублировать код и возможность перезаписывать методы в новые версии api с небольшой работой. Это также экономит необходимость усложнять исходный код с логикой переключения версий. Если вы не закодируете конечную точку в версии, она захватит предыдущую версия по умолчанию.
по сравнению с тем, что делают другие, это кажется проще. Я что-то упускаю?
в произведениях вы можете иметь отрицание. Поэтому для method1 скажите produces="!...1.7"
и в method2 имеют положительное значение.
производит также массив, поэтому вы для method1 можете сказать produces={"...1.6","!...1.7","...1.8"}
etc (принять все, кроме 1.7)
конечно не так идеально как диапазоны, которые вы имеете в виду, но я думаю, что проще в обслуживании, чем другие пользовательские вещи, если это что-то необычное в вашей системе. Удачи!
вы можете использовать AOP, вокруг перехвата
рассмотрите возможность отображения запроса, который получает все /**/public_api/*
и в этом методе ничего не делать;
@RequestMapping({
"/**/public_api/*"
})
public void method2(Model model){
}
после
@Override
public void around(Method method, Object[] args, Object target)
throws Throwable {
// look for the requested version from model parameter, call it desired range
// check the target object for @VersionRange annotation with reflection and acquire version ranges, call the function if it is in the desired range
}
единственное ограничение заключается в том, что все должно быть в одном контроллере.
настройки АОП посмотреть http://www.mkyong.com/spring/spring-aop-examples-advice/