Как управлять версиями REST API с помощью Spring?

119

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

Аугусто
источник
1
@flup Я не понимаю вашего комментария. Это просто говорит о том, что вы можете использовать заголовки, и, как я уже сказал, того, что Spring предоставляет из коробки, недостаточно для поддержки API, которые постоянно обновляются. Что еще хуже, ссылка на этот ответ использует версию в URL-адресе.
Августо
Возможно, это не совсем то, что вы ищете, но Spring 3.2 поддерживает параметр «производит» в RequestMapping. Единственное предостережение: список версий должен быть явным. Eg, produces={"application/json-1.0", "application/json-1.1"}etc
bimsapi
1
Нам необходимо поддерживать несколько версий наших API-интерфейсов, эти различия обычно представляют собой незначительные изменения, которые могут сделать некоторые вызовы от некоторых клиентов несовместимыми (не будет странным, если нам потребуется поддерживать 4 второстепенных версии, в которых некоторые конечные точки несовместимы). Я ценю предложение поместить его в URL-адрес, но мы знаем, что это шаг в неправильном направлении, поскольку у нас есть несколько приложений с версией в URL-адресе, и каждый раз, когда нам нужно подтолкнуть версия.
Августо
1
@ Аугусто, ты тоже этого не сделал. Просто спроектируйте свои изменения API таким образом, чтобы не нарушить обратную совместимость. Просто приведите мне пример изменений, нарушающих совместимость, и я покажу вам, как внести эти изменения без нарушения правил.
Алексей Андреев

Ответы:

62

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

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

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

  1. Создайте новую аннотацию VersionRange.
  2. Реализуйте RequestCondition<VersionRange>. Поскольку у вас будет что-то вроде алгоритма наилучшего соответствия, вам нужно будет проверить, обеспечивают ли методы, аннотированные другими VersionRangeзначениями, лучшее соответствие текущему запросу.
  3. Реализуйте на VersionRangeRequestMappingHandlerMappingоснове аннотации и условия запроса (как описано в сообщении Как реализовать настраиваемые свойства @RequestMapping ).
  4. Настройте spring для оценки VersionRangeRequestMappingHandlerMappingперед использованием значения по умолчанию RequestMappingHandlerMapping(например, установив его порядок на 0).

Это не потребует каких-либо хакерских замен компонентов Spring, но использует механизмы конфигурации и расширения Spring, поэтому он должен работать, даже если вы обновляете свою версию Spring (если новая версия поддерживает эти механизмы).

xwoker
источник
Спасибо, что добавили свой комментарий в качестве ответа xwoker. На данный момент лучший. Я реализовал решение на основе упомянутых вами ссылок, и это не так уж плохо. Самая большая проблема проявится при обновлении до новой версии Spring, так как потребуется проверить любые изменения в логике mvc:annotation-driven. Надеюсь, Spring предоставит версию, mvc:annotation-drivenв которой можно определять собственные условия.
Аугусто
@ Аугусто, полгода спустя, как это у тебя работает? Кроме того, мне любопытно, действительно ли вы управляете версиями для каждого метода? На данный момент мне интересно, не будет ли яснее версия для уровня детализации на уровне класса / контроллера?
Sander Verhagen
1
@SanderVerhagen работает, но мы создаем версию всего API, а не для каждого метода или контроллера (API довольно маленький, поскольку он ориентирован на один аспект бизнеса). У нас есть значительно более крупный проект, в котором они решили использовать другую версию для каждого ресурса и указать это в URL-адресе (так что вы можете иметь конечную точку в / v1 / sessions, а другой ресурс в совершенно другой версии, например / v4 / orders) ... он немного более гибкий, но он заставляет клиентов знать, какую версию вызывать для каждой конечной точки.
Августо
1
К сожалению, это не очень хорошо работает со Swagger, поскольку многие функции автоматической настройки отключаются при расширении WebMvcConfigurationSupport.
Рик
Я пробовал это решение, но на самом деле оно не работает с 2.3.2.RELEASE. У вас есть какой-нибудь пример проекта, чтобы показать?
Патрик
54

Я только что создал индивидуальное решение. Я использую @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

}

Реализация:

Аннотация ApiVersion.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");
    }
}
Бенджамин М
источник
4
Я изменил int [] на String [], чтобы разрешить использование таких версий, как «1.2», и поэтому я могу обрабатывать такие ключевые слова, как «latest»
Maelig
3
Да, это вполне разумно. Для будущих проектов я бы пошел другим путем по некоторым причинам: 1. URL-адреса представляют ресурсы. /v1/aResourceи /v2/aResourceвыглядят как разные ресурсы, но это просто другое представление одного и того же ресурса! 2. Использование заголовков HTTP выглядит лучше, но вы не можете дать кому-либо URL-адрес, потому что URL-адрес не содержит заголовка. 3. Использование параметра URL, то есть /aResource?v=2.1(кстати: так Google делает версии). ...Я все еще не уверен, что выберу вариант 2 или 3 , но я больше никогда не буду использовать 1 по причинам, указанным выше.
Бенджамин М
5
Если вы хотите вставить свои собственные RequestMappingHandlerMappingв свой WebMvcConfiguration, вы должны перезаписать createRequestMappingHandlerMappingвместо requestMappingHandlerMapping! В противном случае вы столкнетесь со странными проблемами (у меня внезапно возникли проблемы с ленивой инициализацией Hibernates из-за закрытого сеанса)
stuXnet
1
Подход выглядит неплохо, но почему-то не работает с тестовыми примерами junti (SpringRunner). Есть шанс, что у вас есть подход, работающий с тестовыми
примерами
1
Есть способ сделать эту работу: не расширять, WebMvcConfigurationSupport а расширять DelegatingWebMvcConfiguration. Это работает для меня (см stackoverflow.com/questions/22267191/... )
SeB.Fr
17

Я бы по-прежнему рекомендовал использовать URL-адреса для управления версиями, потому что в URL-адресах @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;
    }
}

На основе последнего подхода вы можете реализовать что-то вроде того, что хотите.

Например, у вас может быть контроллер, содержащий только методы с обработкой версий.

В этой обработке вы смотрите (используя библиотеки отражения / AOP / генерации кода) в каком-то весеннем сервисе / компоненте или в том же классе для метода с тем же именем / подписью и требуемым @VersionRange и вызываете его, передавая все параметры.

неуловимый-код
источник
14

Я реализовал решение, которое ИДЕАЛЬНО решает проблему с оставшимися версиями.

Общие положения Есть 3 основных подхода к управлению версиями в остальном:

  • Подход на основе пути , при котором клиент определяет версию в URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Заголовок Content-Type , в котором клиент определяет версию в заголовке Accept :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Пользовательский заголовок , в котором клиент определяет версию в настраиваемом заголовке.

Проблема с первым подходом заключается в том, что если вы измените версию , скажем , от v1 -> v2, вероятно , вам нужно скопировать и вставить в v1 ресурсах, которые не изменились на путь v2

Проблема с вторым подходом заключается в том, что некоторые инструментах , такие как http://swagger.io/ не может различить между операциями с таким же путем , но разными Content-Type (проверка выпуск https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

Решение

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

Допустим, у нас есть версии v1 и v2 для контроллера User:

package com.mspapant.example.restVersion.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * The user controller.
 *
 * @author : Manos Papantonakos on 19/8/2016.
 */
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController {

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v1/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV1() {
         return "User V1";
    }

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v2/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV2() {
         return "User V2";
    }
 }

Требование , если я просить v1 для ресурса пользователя я должен принять «V1» Пользователь repsonse, в противном случае , если я просить v2 , v3 и так далее я должен принять «V2 пользователя» ответ.

введите описание изображения здесь

Чтобы реализовать это весной, нам нужно переопределить поведение RequestMappingHandlerMapping по умолчанию :

package com.mspapant.example.restVersion.conf.mapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Value("${server.apiContext}")
    private String apiContext;

    @Value("${server.versionContext}")
    private String versionContext;

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        HandlerMethod method = super.lookupHandlerMethod(lookupPath, request);
        if (method == null && lookupPath.contains(getApiAndVersionContext())) {
            String afterAPIURL = lookupPath.substring(lookupPath.indexOf(getApiAndVersionContext()) + getApiAndVersionContext().length());
            String version = afterAPIURL.substring(0, afterAPIURL.indexOf("/"));
            String path = afterAPIURL.substring(version.length() + 1);

            int previousVersion = getPreviousVersion(version);
            if (previousVersion != 0) {
                lookupPath = getApiAndVersionContext() + previousVersion + "/" + path;
                final String lookupFinal = lookupPath;
                return lookupHandlerMethod(lookupPath, new HttpServletRequestWrapper(request) {
                    @Override
                    public String getRequestURI() {
                        return lookupFinal;
                    }

                    @Override
                    public String getServletPath() {
                        return lookupFinal;
                    }});
            }
        }
        return method;
    }

    private String getApiAndVersionContext() {
        return "/" + apiContext + "/" + versionContext;
    }

    private int getPreviousVersion(final String version) {
        return new Integer(version) - 1 ;
    }

}

Реализация считывает версию в URL-адресе и просит весной разрешить URL-адрес. Если этот URL-адрес не существует (например, клиент запросил v3 ), мы пробуем с v2 и так далее, пока не найдем самую последнюю версию для ресурса. ,

Чтобы увидеть преимущества этой реализации, скажем, у нас есть два ресурса: Пользователь и Компания:

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company

Допустим, мы внесли изменение в «контракт» компании, которое разрывает клиента. Поэтому мы реализуем http://localhost:9001/api/v2/companyи просим клиента перейти на v2 вместо v1.

Итак, новые запросы от клиента:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

вместо того:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

Самое приятное здесь то, что с этим решением клиент будет получать информацию о пользователе из v1 и информацию о компании из v2 без необходимости создавать новую (такую ​​же) конечную точку от пользователя v2!

Остальная документация Как я уже сказал, причина, по которой я выбираю подход к управлению версиями на основе URL, заключается в том, что некоторые инструменты, такие как swagger, не документируют по-разному конечные точки с одним и тем же URL, но с другим типом контента. В этом решении отображаются обе конечные точки, поскольку имеют разные URL-адреса:

введите описание изображения здесь

GIT

Реализация решения по адресу: https://github.com/mspapant/restVersioningExample/

mspapant
источник
9

@RequestMappingАннотацию поддерживает headersэлемент , который позволяет сузить запросы соответствия. В частности, вы можете использовать Acceptздесь заголовок.

@RequestMapping(headers = {
    "Accept=application/vnd.company.app-1.0+json",
    "Accept=application/vnd.company.app-1.1+json"
})

Это не совсем то, что вы описываете, поскольку он не обрабатывает диапазоны напрямую, но элемент поддерживает подстановочный знак *, а также! =. Так что, по крайней мере, вы могли бы избежать использования подстановочного знака для случаев, когда все версии поддерживают рассматриваемую конечную точку или даже все второстепенные версии данной основной версии (например, 1. *).

Не думаю, что я действительно использовал этот элемент раньше (если и использовал, не помню), поэтому я просто ухожу от документации на

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html


источник
2
Я знаю об этом, но, как вы отметили, для каждой версии мне нужно было бы перейти ко всем моим контроллерам и добавить версию, даже если они не изменились. Диапазон, который вы упомянули, работает только с полным шрифтом, например, application/*а не с его частями. Например, в Spring недопустимо следующее "Accept=application/vnd.company.app-1.*+json". Это связано как класс весенних MediaTypeработ
Аугусто
@Augusto, вам не обязательно это делать. При таком подходе вы управляете версиями не «API», а «конечной точки». У каждой конечной точки может быть своя версия. Для меня это самый простой способ версии API по сравнению с версией API . Swagger также проще настроить . Эта стратегия называется управлением версиями посредством согласования содержимого.
Dherik
3

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

@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 с минимальными усилиями. Это также избавляет от необходимости усложнять исходный код с помощью логики переключения версий. Если вы не закодируете конечную точку в версии, она по умолчанию захватит предыдущую версию.

По сравнению с тем, что делают другие, это кажется намного проще. Что-то мне не хватает?

Ceekay
источник
1
+1 за то, что поделился кодом. Однако наследование тесно связывает это. Вместо. Контроллеры (Test1 и Test2) должны быть просто проходными ... без логической реализации. Вся логика должна быть в классе обслуживания someService. В этом случае просто используйте простую композицию и никогда не наследуйте от другого контроллера
Дэн Хьюнекс
1
@ dan-hunex Похоже, Ceekay использует наследование для управления различными версиями api. Если вы удалите наследование, какое решение? И почему тесная пара в этом примере является проблемой? С моей точки зрения, Test2 расширяет Test1, потому что это его улучшение (с той же ролью и теми же обязанностями), не так ли?
jeremieca
2

Я уже пробовал версию своего API, используя управление версиями URI , например:

/api/v1/orders
/api/v2/orders

Но при попытке заставить эту работу работать возникают некоторые проблемы: как организовать свой код с разными версиями? Как управлять двумя (или более) версиями одновременно? Какое влияние при удалении какой-то версии?

Лучшей альтернативой, которую я нашел, была не версия всего API, а управление версией на каждой конечной точке . Этот шаблон называется управлением версиями с использованием заголовка Accept или управлением версиями через согласование содержимого :

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

Реализация на Spring

Сначала вы создаете контроллер с базовым атрибутом productions, который будет применяться по умолчанию для каждой конечной точки внутри класса.

@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {

}

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

@Deprecated
@PostMapping
public ResponseEntity<OrderResponse> createV1(
        @RequestBody OrderRequest orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PostMapping(
        produces = "application/vnd.company.etc.v2+json",
        consumes = "application/vnd.company.etc.v2+json")
public ResponseEntity<OrderResponseV2> createV2(
        @RequestBody OrderRequestV2 orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

Готово! Просто вызовите каждую конечную точку, используя нужную версию заголовка Http :

Content-Type: application/vnd.company.etc.v1+json

Или, чтобы назвать версию два:

Content-Type: application/vnd.company.etc.v2+json

О ваших заботах:

И поскольку не все методы в API меняются в одном и том же выпуске, я не хочу переходить к каждому из моих контроллеров и изменять что-либо для обработчика, который не изменился между версиями.

Как объяснялось, эта стратегия поддерживает каждый контроллер и конечную точку с его фактической версией. Вы изменяете только ту конечную точку, которая имеет модификации и нуждается в новой версии.

А развязность?

С помощью этой стратегии также очень легко настроить Swagger с разными версиями. См. Этот ответ для получения более подробной информации.

Dherik
источник
1

В производстве вы можете иметь отрицание. Итак, для метода 1 говорят, produces="!...1.7"а для метода 2 положительные.

Производит также массив, поэтому вы для метода 1 можете сказать и produces={"...1.6","!...1.7","...1.8"}т.д. (принять все, кроме 1.7)

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

codesalsa
источник
Спасибо codealsa, я пытаюсь найти способ, который прост в обслуживании и не требует обновления каждой конечной точки каждый раз, когда нам нужно поднять версию.
Августо
0

Вы можете использовать АОП вокруг перехвата

Рассмотрите возможность отображения запроса, который получает все /**/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/

Hevi
источник
Спасибо, hevi, я ищу более «весенний» способ сделать это, поскольку Spring уже выбирает, какой метод вызывать без использования АОП. Я считаю, что АОП добавляет новый уровень сложности кода, которого я бы хотел избежать.
Августа
@Augusto, Spring имеет отличную поддержку АОП. Тебе стоит попробовать. :)
Константин Йовков