Як керувати версіями REST API з весною?


118

Я шукав, як керувати версіями REST API, використовуючи Spring 3.2.x, але не знайшов нічого, що було б просто в обслуговуванні. Я поясню спершу проблему, яку я маю, а потім вирішення ... але мені цікаво, чи я не буду винаходити колесо тут.

Я хочу керувати версією на основі заголовка Accept, і, наприклад, якщо запит має заголовок Accept application/vnd.company.app-1.1+json, я хочу, щоб весняний 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;
}

Це неможливо навесні, оскільки два способи мають однакову RequestMappingанотацію, і Spring не вдається завантажити. Ідея полягає в тому, що в 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 вже є багато логіки, щоб вибрати метод контролера, який потрібно викликати, і я хочу зробити це.


Редагувати 2

Я поділився оригінальним POC (з деякими вдосконаленнями) в github: https://github.com/augusto/restVersioning



1
@flup Я не розумію ваш коментар. Це просто говорить про те, що ви можете використовувати заголовки, і, як я вже сказав, те, що весна надається поза коробкою, недостатньо для підтримки API, які постійно оновлюються. Ще гірше, що посилання на цю відповідь використовує версію в URL.
Август

Можливо, не саме те, що ви шукаєте, але Spring 3.2 підтримує параметр "виробляє" в RequestMapping. Одне застереження полягає в тому, що список версій повинен бути явним. Наприклад, produces={"application/json-1.0", "application/json-1.1"}тощо
bimsapi

1
Нам потрібно підтримувати кілька версій наших API, ці відмінності, як правило, є незначними змінами, які роблять деякі дзвінки від деяких клієнтів несумісними (це не дивно, якщо нам потрібно підтримувати 4 незначні версії, в яких деякі кінцеві точки несумісні). Я вдячний за пропозицію розмістити його в URL-адресі, але ми знаємо, що це крок у неправильному напрямку, оскільки у нас є декілька додатків з версією в URL-адресі, і ми маємо багато роботи, щоразу, коли нам потрібно зіткнути версія.
Август

1
@Augusto, ти насправді у тебе теж не надто. Просто спроектуйте, що ваш API змінює спосіб, який не порушує зворотну сумісність. Просто наведіть мені приклад змін, які порушують сумісність, і я покажу вам, як внести ці зміни в безперебійний спосіб.
Олексій Андрєєв

Відповіді:


62

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

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

Як описано в цій відповіді ТА, ви насправді можете мати те ж саме @RequestMappingі використовувати різну примітку для розмежування під час фактичної маршрутизації, що відбувається під час виконання. Для цього вам доведеться:

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

Для цього не потрібно будь-яких задушливих замін компонентів Spring, але використовуються механізми конфігурації та розширення Spring, тому це повинно працювати, навіть якщо ви оновлюєте свою версію Spring (доки нова версія підтримує ці механізми).


Дякуємо, що додали ваш коментар як відповідь xwoker. На сьогоднішній день найкращий. Я реалізував рішення на основі згаданих вами посилань, і це не так вже й погано. Найбільша проблема виявиться при переході на нову версію Spring, оскільки вона вимагатиме перевірити будь-які зміни в логіці, що стоїть mvc:annotation-driven. Сподіваємось, Весна запропонує версію, mvc:annotation-drivenв якій можна визначити власні умови.
Августо

@Augusto, через півроку, як це у вас вийшло? Також мені цікаво, чи справді ви версійте на основі методу? На даний момент мені цікаво, чи не було б зрозумілішою версія для деталізації рівня класу / контролера?
Сандер Верхаген

1
@SanderVerhagen працює, але ми робимо версію всього API, а не за методом чи контролером (API досить малий, оскільки він зосереджений на одному аспекті бізнесу). У нас є значно більший проект, де вони вирішили використовувати іншу версію на ресурс і вказати це в URL-адресі (так що ви можете мати кінцеву точку на / v1 / сеанси та інший ресурс у зовсім іншій версії, наприклад / v4 / замовлення) ... це трохи гнучкіше, але це робить більший тиск на клієнтів, щоб знати, яку версію зателефонувати для кожної кінцевої точки.
Августо

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", і тому я можу обробляти такі ключові слова, як "найновіший"
Maelig

3
Так, це цілком розумно. Для майбутніх проектів я б пішов іншим шляхом з деяких причин: 1. URL-адреси представляють ресурси. /v1/aResourceі /v2/aResourceвиглядають як різні ресурси, але це просто різне представлення одного і того ж ресурсу! 2. Використання заголовків HTTP виглядає краще, але ви не можете надати комусь URL, оскільки URL не містить заголовка. 3. Використовуючи параметр URL, тобто /aResource?v=2.1(btw: таким чином Google робить версію)....Я до сих пір не впевнений , якщо я піду з опцією 2 або 3 , але я ніколи не буду використовувати 1 раз з причин , зазначеним вище.
Бенджамін М

5
Якщо ви хочете ввести своє RequestMappingHandlerMappingв своє WebMvcConfiguration, замість createRequestMappingHandlerMappingнього слід перезаписати requestMappingHandlerMapping! Інакше у вас виникнуть дивні проблеми (у мене раптом виникли проблеми з ініціалізацією лінивої системи Hibernates через закриту сесію)
stuXnet

1
Підхід виглядає добре, але якось здається, він не працює з тестовими кейсами junti (SpringRunner). Будь-який шанс, що у вас є підхід у роботі з тестовими кейсами
JDev

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

Я реалізував рішення, яке справляється ДОБРО проблему з іншим версією.

Загально кажучи, існує три основні підходи до версії спокою:

  • Підтвердження на основі шляху , в якому клієнт визначає версію в 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 для контролера користувача:

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/


9

@RequestMappingАнотацію підтримує headersелемент , який дозволяє звузити запити відповідності. Зокрема, тут можна використовувати Acceptзаголовок.

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

Це не зовсім те, що ви описуєте, оскільки воно не обробляє безпосередньо діапазони, але елемент підтримує * wildcard так само, як! =. Так, принаймні, ви можете уникнути використання підстановки для випадків, коли всі версії підтримують кінцеву точку, про яку йдеться, або навіть усі незначні версії даної основної версії (наприклад, 1. *).

Я не думаю, що раніше я фактично використовував цей елемент (якщо він не пам’ятаю), тому я просто знімаю документацію на

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


2
Я знаю про це, але, як ви зазначали, мені потрібно було б перейти до всіх своїх контролерів і додати версію, навіть якщо вони не змінилися. Діапазон, про який ви згадали, працює лише на повний тип, наприклад, application/*а не на частини цього типу. Наприклад, наступне недійсне навесні "Accept=application/vnd.company.app-1.*+json". Це пов'язано як клас весняних MediaTypeробіт
Аугусто

@Августо вам не обов’язково робити цього. При такому підході ви не версію "API", а "Endpoint". Кожна кінцева точка може мати різну версію. Для мене це найпростіший спосіб версії API порівняно з версією API . Swagger також простіший в налаштуванні . Ця стратегія називається Версія за допомогою узгодження контенту.
Дерік

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 з невеликою роботою. Це також економить необхідність ускладнювати ваш вихідний код логікою перемикання версій. Якщо ви не кодуєте кінцеву точку у версії, вона перехопить попередню версію за замовчуванням.

У порівнянні з тим, що роблять інші, це здається набагато простішим. Щось мені не вистачає?


1
+1 для спільного використання коду. Однак спадкування щільно з’єднує його. Натомість. Контролери (Test1 і Test2) повинні бути просто проходженням ... ніякої логічної реалізації. Вся логіка повинна бути в сервісному класі, у деяких послугах. У цьому випадку просто використовуйте просту композицію і ніколи не успадковуйте її від іншого контролера
Dan Hunex

1
@ dan-hunex Схоже, Ceekay використовує спадщину для управління різними версіями api. Якщо ви видалите спадщину, яке рішення? І чому щільно пара є проблемою в цьому прикладі? З моєї точки зору, Test2 розширює Test1, тому що це його вдосконалення (з тією ж роллю та однаковими обов'язками), чи не так?
jeremieca

2

Я вже спробував виконати версію свого API за допомогою URI- версії , наприклад:

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

Але при спробі зробити цю роботу є деякі проблеми: як організувати свій код за різними версіями? Як керувати двома (або більше) версіями одночасно? Який вплив при видаленні якоїсь версії?

Найкраща альтернатива, яку я знайшов, - це не версія всього API, а керування версією на кожній кінцевій точці . Цей шаблон називається Версія за допомогою заголовка Accept або Версія за допомогою узгодження вмісту :

Цей підхід дозволяє нам версію єдиного представлення ресурсів замість версії всього API, що дає нам більш детальний контроль над версією. Це також створює менший слід у кодовій базі, оскільки нам не потрібно роздрібнювати всю програму під час створення нової версії. Ще одна перевага цього підходу полягає в тому, що він не вимагає впровадження правил маршрутизації URI, введених версією через шлях URI.

Впровадження навесні

По-перше, ви створюєте контролер з базовим атрибутом proizvod, який застосовуватиметься за замовчуванням для кожної кінцевої точки всередині класу.

@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 з різними версіями також дуже просто, використовуючи цю стратегію. Дивіться цю відповідь для отримання більш детальної інформації.


1

У виробництві ви можете мати заперечення. Так для method1 скажімоproduces="!...1.7" а у method2 - позитивний.

Виробництво також є масивом, так що ви можете для method1 ви можете сказати produces={"...1.6","!...1.7","...1.8"} і т. (Прийміть усі, крім 1,7)

Звичайно, не настільки ідеальні, як діапазони, про які ви пам’ятаєте, але я думаю, що їх легше підтримувати, ніж інші спеціальні речі, якщо це щось незвичне у вашій системі. Удачі!


Спасибі codealsa, я намагаюся знайти спосіб, який простий у обслуговуванні і не потребує оновлення кожної кінцевої точки кожного разу, коли нам потрібно нарізати версію.
Август

0

Ви можете використовувати 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/


Дякую хеві, я шукаю більш "весняний" дружній спосіб зробити це, оскільки Spring вже вибирає, який метод викликати, не використовуючи AOP. Я вважаю, що AOP додає новий рівень складності коду, якого я б хотів уникнути.
Августо

@Augusto, Spring має чудову підтримку AOP. Вам варто спробувати. :)
Костянтин Йовков
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.