Как управлять версиями 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.
Я был бы признателен за любые мысли... и особенно, любое предложение сделать это проще, легче поддерживать способ.
Edit
добавить щедрость. Чтобы получить награду, пожалуйста, ответьте на вопрос выше, не предлагая иметь эту логику в самом контроллере. У Spring уже есть много логики, чтобы выбрать, какой метод контроллера вызывать, и я хочу вернуться к этому.
Edit 2
я поделился оригинальным POC (с некоторыми улучшениями) в github:https://github.com/augusto/restVersioning
8 ответов:
независимо от того, можно ли избежать управления версиями, выполнив обратные совместимые изменения (что не всегда возможно, когда вы связаны некоторыми корпоративными рекомендациями или ваши клиенты API реализованы ошибочным образом и сломаются, даже если они не должны), абстрактное требование является интересным:
как я могу сделать отображение пользовательского запроса, которое выполняет произвольные оценки значений заголовка из запроса, не выполняя оценку в методе тело?
как описано в это так ответ вы на самом деле можете иметь то же самое
@RequestMapping
и использовать другую аннотацию, чтобы различать во время фактической маршрутизации, которая происходит во время выполнения. Для этого вам придется:
- создать новую аннотации
VersionRange
.- реализовать
RequestCondition<VersionRange>
. Поскольку у вас будет что-то вроде алгоритма наилучшего соответствия, вам придется проверить, аннотированы ли методы с другимиVersionRange
значения обеспечивают a лучше соответствовать текущему запросу.- реализовать
VersionRangeRequestMappingHandlerMapping
на основе аннотации и условия запроса (как описано в сообщении как реализовать пользовательские свойства @RequestMapping ).- настройка spring для оценки вашего
VersionRangeRequestMappingHandlerMapping
перед использованием по умолчаниюRequestMappingHandlerMapping
(например, установив его порядок до 0).это не потребует каких-либо хакерских замен пружинных компонентов, но использует конфигурацию и расширение пружины механизмы, поэтому он должен работать, даже если вы обновляете свою версию 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 с обработкой версии.
в этой обработке вы смотрите (используя библиотеки reflection/AOP/Code generation) в некоторых spring service/component или в том же классе для метода с тем же именем / подписью и обязательным @VersionRange и вызвать его, передавая все параметры.
я реализовал решение, которое обрабатывает отлично проблема с управлением версиями rest.
вообще говоря, есть 3 основных подхода для управления версиями rest:
путь-основанный approch, в котором клиент определяет версию в URL:
http://localhost:9001/api/v1/user http://localhost:9001/api/v2/user
Content-Type заголовок, в котором клиент определяет версию в принимать заголовок:
http://localhost:9001/api/v1/user with Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
Заголовок, в котором клиент определяет версию в пользовательский заголовок.
The
The
@RequestMapping
аннотация поддерживает aheaders
элемент, который позволяет сократить соответствующие запросы. В частности, вы можете использовать
Как насчет просто использования наследования для моделирования управления версиями? Это то, что я использую в своем проекте, и он не требует специальной конфигурации spring и получает именно то, что я хочу.
@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 с небольшой работой. Это также избавляет от необходимости усложнять исходный код с помощью логики переключения версий. Если вы не закодируете конечную точку в версии, она захватит предыдущую версия по умолчанию.
по сравнению с тем, что делают другие, это кажется намного проще. Есть ли что-то, что я упускаю?
в произведениях вы можете иметь отрицание. Поэтому для метода 1 говорят
produces="!...1.7"
а в методе 2 есть положительные.производит также массив, так что вы для 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 }
единственное ограничение заключается в том, что все должно быть в одном контроллере.
для конфигурации AOP посмотрите на http://www.mkyong.com/spring/spring-aop-examples-advice/