Как управлять версиями 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 88

8 ответов:

независимо от того, можно ли избежать управления версиями, выполнив обратные совместимые изменения (что не всегда возможно, когда вы связаны некоторыми корпоративными рекомендациями или ваши клиенты API реализованы ошибочным образом и сломаются, даже если они не должны), абстрактное требование является интересным:

как я могу сделать отображение пользовательского запроса, которое выполняет произвольные оценки значений заголовка из запроса, не выполняя оценку в методе тело?

как описано в это так ответ вы на самом деле можете иметь то же самое @RequestMapping и использовать другую аннотацию, чтобы различать во время фактической маршрутизации, которая происходит во время выполнения. Для этого вам придется:

  1. создать новую аннотации VersionRange.
  2. реализовать RequestCondition<VersionRange>. Поскольку у вас будет что-то вроде алгоритма наилучшего соответствия, вам придется проверить, аннотированы ли методы с другими VersionRange значения обеспечивают a лучше соответствовать текущему запросу.
  3. реализовать VersionRangeRequestMappingHandlerMapping на основе аннотации и условия запроса (как описано в сообщении как реализовать пользовательские свойства @RequestMapping ).
  4. настройка 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 аннотация поддерживает a headers элемент, который позволяет сократить соответствующие запросы. В частности, вы можете использовать

Как насчет просто использования наследования для моделирования управления версиями? Это то, что я использую в своем проекте, и он не требует специальной конфигурации 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/