Jak zarządzać wersjami REST API za pomocą Springa?

119

Szukałem, jak zarządzać wersjami REST API za pomocą Spring 3.2.x, ale nie znalazłem niczego, co byłoby łatwe w utrzymaniu. Najpierw wyjaśnię problem, który mam, a potem rozwiązanie ... ale zastanawiam się, czy tu ponownie wymyślam koło.

Chcę zarządzać wersją opartą na nagłówku Accept i na przykład, jeśli żądanie ma nagłówek Accept application/vnd.company.app-1.1+json, chcę, aby Spring MVC przekazał to do metody, która obsługuje tę wersję. A ponieważ nie wszystkie metody w API zmieniają się w tym samym wydaniu, nie chcę przechodzić do każdego z moich kontrolerów i zmieniać niczego dla procedury obsługi, która nie zmieniła się między wersjami. Nie chcę też mieć logiki, która pozwoliłaby na ustalenie, której wersji użyć w samym kontrolerze (używając lokalizatorów usług), ponieważ Spring już odkrywa, którą metodę wywołać.

Więc wziąwszy API z wersjami 1.0 do 1.8, gdzie handler został wprowadzony w wersji 1.0 i zmodyfikowany w v1.7, chciałbym zająć się tym w następujący sposób. Wyobraź sobie, że kod znajduje się wewnątrz kontrolera i że istnieje kod, który jest w stanie wyodrębnić wersję z nagłówka. (Poniższe informacje są nieważne na wiosnę)

@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;
}

Nie jest to możliwe wiosną, ponieważ obie metody mają takie same RequestMappingadnotacje, a sprężyna nie ładuje się. Chodzi o to, że VersionRangeadnotacja może definiować otwarty lub zamknięty zakres wersji. Pierwsza metoda obowiązuje od wersji 1.0 do 1.6, a druga dla wersji 1.7 i nowszych (w tym najnowszej wersji 1.8). Wiem, że to podejście zawodzi, jeśli ktoś zdecyduje się przekazać wersję 99.99, ale to jest coś, z czym mogę żyć.

Teraz, ponieważ powyższe nie jest możliwe bez poważnej zmiany sposobu działania sprężyny, myślałem o majstrowaniu przy sposobie dopasowywania programów obsługi do żądań, w szczególności o pisaniu własnych ProducesRequestConditioni posiadaniu tam zakresu wersji. Na przykład

Kod:

@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;
}

W ten sposób mogę mieć zamknięte lub otwarte zakresy wersji zdefiniowane w części adnotacji produkującej. Pracuję nad tym roztworze teraz z problemem, że miałem jeszcze wymienić kilka podstawowych klas wiosny MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappingi RequestMappingInfo), która mi się nie podoba, bo to oznacza dodatkową pracę, gdy zdecyduję się uaktualnienie do nowszej wersji wiosna.

Byłbym wdzięczny za wszelkie przemyślenia ... a zwłaszcza wszelkie sugestie, aby zrobić to w prostszy, łatwiejszy do utrzymania sposób.


Edytować

Dodanie nagrody. Aby otrzymać nagrodę, odpowiedz na powyższe pytanie bez sugerowania posiadania tej logiki w samym kontrolerze. Spring ma już dużo logiki, aby wybrać metodę kontrolera do wywołania i chcę na to przejść.


Edytuj 2

Udostępniłem oryginalny POC (z pewnymi ulepszeniami) na github: https://github.com/augusto/restVersioning

Augusto
źródło
1
@flup Nie rozumiem twojego komentarza. To tylko mówi, że możesz używać nagłówków i, jak powiedziałem, to, co dostarcza wiosna po wyjęciu z pudełka, nie jest wystarczające do obsługi API, które są stale aktualizowane. Co gorsza, odsyłacz w tej odpowiedzi wykorzystuje wersję w adresie URL.
Augusto
Może nie jest to dokładnie to, czego szukasz, ale Spring 3.2 obsługuje parametr „produkuje” w RequestMapping. Jedynym zastrzeżeniem jest to, że lista wersji musi być jednoznaczna. Np. produces={"application/json-1.0", "application/json-1.1"}Itd.
bimsapi
1
Musimy obsługiwać kilka wersji naszych API, te różnice to zwykle drobne zmiany, które sprawią, że niektóre wywołania od niektórych klientów będą niekompatybilne (nie będzie dziwne, jeśli będziemy potrzebować obsługi 4 pomniejszych wersji, w których niektóre punkty końcowe są niekompatybilne). Doceniam sugestię umieszczenia go w adresie URL, ale wiemy, że jest to krok w złym kierunku, ponieważ mamy kilka aplikacji z wersją w adresie URL i za każdym razem, gdy musimy uderzyć w wersja.
Augusto
1
@Augusto, właściwie ty też nie. Po prostu zaprojektuj swój interfejs API w sposób, który nie łamie wstecznej zgodności. Podaj mi tylko przykład zmian, które łamią kompatybilność, a pokażę ci, jak wprowadzić te zmiany w niezłomny sposób.
Alexey Andreev

Odpowiedzi:

62

Bez względu na to, czy można uniknąć wersjonowania, wprowadzając zmiany zgodne z poprzednimi wersjami (co może nie zawsze być możliwe, gdy jesteś związany pewnymi wytycznymi korporacyjnymi lub klienci API są zaimplementowani w sposób błędny i zepsują się, nawet jeśli nie powinni), abstrakcyjne wymaganie jest interesujące jeden:

Jak mogę wykonać niestandardowe mapowanie żądania, które wykonuje arbitralne oceny wartości nagłówka z żądania bez przeprowadzania oceny w treści metody?

Jak opisano w tej odpowiedzi SO , w rzeczywistości możesz mieć to samo @RequestMappingi użyć innej adnotacji, aby rozróżnić podczas rzeczywistego routingu, który ma miejsce w czasie wykonywania. Aby to zrobić, będziesz musiał:

  1. Utwórz nową adnotację VersionRange.
  2. Zaimplementuj RequestCondition<VersionRange>. Ponieważ będziesz mieć coś w rodzaju algorytmu najlepiej dopasowanego, będziesz musiał sprawdzić, czy metody są opatrzone adnotacjami innymiVersionRange wartościami zapewniają lepsze dopasowanie do bieżącego żądania.
  3. Zaimplementuj na VersionRangeRequestMappingHandlerMappingpodstawie adnotacji i warunku żądania (zgodnie z opisem w poście Jak zaimplementować niestandardowe właściwości @RequestMapping ).
  4. Skonfiguruj sprężynę, aby oceniała twój VersionRangeRequestMappingHandlerMappingprzed użyciem domyślnego RequestMappingHandlerMapping(np. Ustawiając jego kolejność na 0).

Nie wymagałoby to żadnych hackerskich zamienników komponentów Springa, ale wykorzystuje mechanizmy konfiguracji i rozszerzeń Springa, więc powinno działać nawet jeśli zaktualizujesz wersję Springa (o ile nowa wersja obsługuje te mechanizmy).

xwoker
źródło
Dzięki za dodanie komentarza jako odpowiedzi xwoker. Jak dotąd jest najlepszy. Wdrożyłem rozwiązanie w oparciu o wspomniane przez Ciebie linki i nie jest takie złe. Największy problem pojawi się podczas aktualizacji do nowej wersji Springa, ponieważ będzie to wymagało sprawdzenia wszelkich zmian w logice mvc:annotation-driven. Miejmy nadzieję, że Spring dostarczy wersję, mvc:annotation-drivenw której można zdefiniować niestandardowe warunki.
Augusto
@Augusto, pół roku później, jak ci się to udało? Jestem też ciekawy, czy naprawdę używasz wersji na podstawie metody? W tym momencie zastanawiam się, czy nie byłoby jaśniejsze, aby wersja na poziomie szczegółowości na klasę / kontroler?
Sander Verhagen
1
@SanderVerhagen to działa, ale tworzymy wersję całego API, a nie według metody lub kontrolera (API jest dość małe, ponieważ koncentruje się na jednym aspekcie biznesu). Mamy znacznie większy projekt, w którym zdecydowali się użyć innej wersji na zasób i określili to w adresie URL (abyś mógł mieć punkt końcowy na sesjach / v1 / i inny zasób w zupełnie innej wersji, np. / V4 / orders) ... jest nieco bardziej elastyczny, ale wywiera większy nacisk na klientów, aby wiedzieli, którą wersję wywołać z każdego punktu końcowego.
Augusto
1
Niestety, nie działa to dobrze w Swagger, ponieważ wiele automatycznych konfiguracji jest wyłączanych podczas rozszerzania WebMvcConfigurationSupport.
Rick
Wypróbowałem to rozwiązanie, ale w rzeczywistości nie działa z wersją 2.3.2.RELEASE. Masz jakiś przykładowy projekt do pokazania?
Patrick
54

Właśnie stworzyłem niestandardowe rozwiązanie. Używam @ApiVersionadnotacji w połączeniu z @RequestMappingadnotacją wewnętrzną@Controller klas.

Przykład:

@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

}

Realizacja:

Adnotacja ApiVersion.java :

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java (jest to głównie kopiowanie i wklejanie 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);
    }

}

Wstrzyknięcie do WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}
Benjamin M.
źródło
4
Zmieniłem int [] na String [], aby zezwolić na wersje takie jak „1.2”, więc mogę obsługiwać słowa kluczowe, takie jak „najnowsze”
Maelig
3
Tak, to całkiem rozsądne. W przypadku przyszłych projektów wybrałbym inną drogę z kilku powodów: 1. Adresy URL reprezentują zasoby. /v1/aResourcei /v2/aResourcewyglądają jak różne zasoby, ale to tylko inna reprezentacja tego samego zasobu! 2. Używanie nagłówków HTTP wygląda lepiej, ale nie możesz dać komuś adresu URL, ponieważ URL nie zawiera nagłówka. 3. Użycie parametru adresu URL, tj. /aResource?v=2.1(Btw: w ten sposób Google wykonuje wersjonowanie). ...Nadal nie jestem pewien, czy wybrałbym opcję 2 czy 3 , ale nigdy więcej nie użyję 1 z powodów wymienionych powyżej.
Benjamin M.
5
Jeśli chcesz, aby wprowadzić swój własny RequestMappingHandlerMappingdo swojego WebMvcConfiguration, należy nadpisać createRequestMappingHandlerMappingzamiast requestMappingHandlerMapping! W przeciwnym razie napotkasz dziwne problemy (nagle miałem problemy z leniwą inicjalizacją Hibernates z powodu zamkniętej sesji)
stuXnet
1
Podejście wygląda dobrze, ale wydaje się, że nie działa z przypadkami testowymi junti (SpringRunner). Jakakolwiek szansa, że ​​masz podejście do pracy z przypadkami testowymi
JDev
1
Jest sposób, aby to zadziałało, nie przedłużaj, WebMvcConfigurationSupport ale rozszerzaj DelegatingWebMvcConfiguration. To zadziałało dla mnie (patrz stackoverflow.com/questions/22267191/… )
SEB.
17

Nadal zalecałbym używanie adresów URL do wersjonowania, ponieważ w adresach URL @RequestMapping obsługuje wzorce i parametry ścieżki, których format można określić za pomocą wyrażenia regularnego.

A do obsługi aktualizacji klienta (o których wspomniałeś w komentarzu) możesz użyć aliasów, takich jak „najnowsze”. Lub mieć niewersjonowaną wersję interfejsu API, która używa najnowszej wersji (tak).

Używając parametrów ścieżki, możesz zaimplementować dowolną złożoną logikę obsługi wersji, a jeśli już chcesz mieć zakresy, bardzo dobrze możesz chcieć czegoś więcej.

Oto kilka przykładów:

@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;
    }
}

Opierając się na ostatnim podejściu, możesz faktycznie zaimplementować coś takiego, jak chcesz.

Na przykład możesz mieć kontroler, który zawiera tylko stabs metod z obsługą wersji.

W tej obsłudze patrzysz (za pomocą bibliotek refleksji / AOP / generowania kodu) w jakiejś usłudze / komponencie sprężyny lub w tej samej klasie dla metody o tej samej nazwie / sygnaturze i wymaganej @VersionRange i wywołujesz ją przekazując wszystkie parametry.

nieuchwytny-kod
źródło
14

Wdrożyłem rozwiązanie, które IDEALNIE rozwiązuje problem z wersjonowaniem resztek.

Mówiąc ogólnie, istnieją 3 główne podejścia do wersjonowania reszty:

  • Podejście oparte na ścieżce , w którym klient definiuje wersję w adresie URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Nagłówek Content-Type , w którym klient definiuje wersję w nagłówku Accept :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Niestandardowy nagłówek , w którym klient definiuje wersję w niestandardowym nagłówku.

Problem z pierwszego podejścia jest to, że jeśli zmiany wersji powiedzmy od V1 -> v2, prawdopodobnie trzeba skopiować i wkleić zasobów v1, które nie zostały zmienione na ścieżce v2

Problem z drugiego podejścia jest to, że niektóre narzędzia, takie jak http://swagger.io/ nie może odrębny od operacji z tej samej drogi, ale innym Content-Type (emisyjnej check https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

Rozwiązanie

Ponieważ dużo pracuję z narzędziami do dokumentacji odpoczynku, wolę używać pierwszego podejścia. Moje rozwiązanie rozwiązuje problem z pierwszym podejściem, więc nie musisz kopiować i wklejać punktu końcowego do nowej wersji.

Powiedzmy, że mamy wersje v1 i v2 dla kontrolera użytkownika:

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";
    }
 }

Wymogiem jest, jeśli ja zażądać v1 dla zasobu użytkownika muszę wziąć „V1 użytkownika” repsonse, inaczej gdybym zażądać v2 , v3 i tak dalej mam wziąć „V2” User odpowiedź.

wprowadź opis obrazu tutaj

Aby zaimplementować to wiosną, musimy nadpisać domyślne zachowanie 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 ;
    }

}

Realizacja odczytuje wersję w URL i zwraca się od wiosny do rozstrzygnięcia sprawy URL .W ten adres nie istnieje (na przykład żądanie klienta v3 ), a następnie staramy się v2 i tak jeden, aż znajdziemy najnowszą wersję dla zasobu .

Aby zobaczyć korzyści płynące z tego wdrożenia, powiedzmy, że mamy dwa zasoby: użytkownika i firmy:

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

Powiedzmy, że dokonaliśmy zmiany w „kontrakcie” firmy, która łamie klienta. Dlatego implementujemy http://localhost:9001/api/v2/companyi prosimy klienta o zmianę na v2 zamiast na v1.

Tak więc nowe żądania od klienta to:

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

zamiast:

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

Najlepsza część jest to, że z tego rozwiązania klient otrzyma informacje o użytkownikach z v1 i v2 od firmy informacji bez konieczności tworzenia nowego punktu końcowego (samo) od użytkownika v2!

Reszta dokumentacji Jak powiedziałem wcześniej, powodem, dla którego wybrałem podejście do wersjonowania opartego na adresach URL, jest to, że niektóre narzędzia, takie jak swagger, nie dokumentują inaczej punktów końcowych z tym samym adresem URL, ale z innym typem zawartości. W przypadku tego rozwiązania oba punkty końcowe są wyświetlane, ponieważ mają inny adres URL:

wprowadź opis obrazu tutaj

GIT

Wdrożenie rozwiązania pod adresem : https://github.com/mspapant/restVersioningExample/

mspapant
źródło
9

@RequestMappingAdnotacji obsługuje headerselement, który pozwala ograniczyć żądania ogłoszeń. W szczególności możesz tutaj użyć Acceptnagłówka.

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

Nie jest to dokładnie to, co opisujesz, ponieważ nie obsługuje bezpośrednio zakresów, ale element obsługuje zarówno * symbol wieloznaczny, jak i! =. Więc przynajmniej możesz uciec od używania symbolu wieloznacznego w przypadkach, gdy wszystkie wersje obsługują dany punkt końcowy, a nawet wszystkie podrzędne wersje danej wersji głównej (np. 1. *).

Wydaje mi się, że nie korzystałem wcześniej z tego elementu (jeśli mam, nie pamiętam), więc po prostu wychodzę z dokumentacji o godz

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


źródło
2
Wiem o tym, ale jak zauważyłeś, w każdej wersji musiałbym przejść do wszystkich moich kontrolerów i dodać wersję, nawet jeśli się nie zmieniły. Zakres, o którym wspomniałeś, działa tylko w przypadku pełnego application/*tekstu, a nie jego części. Na przykład poniższe informacje są nieprawidłowe na wiosnę "Accept=application/vnd.company.app-1.*+json". Jest to związane z tym, jak MediaTypedziała klasa wiosenna
Augusto
@ Augusto, niekoniecznie musisz to robić. Przy takim podejściu nie tworzysz wersji „API”, ale „Endpoint”. Każdy punkt końcowy może mieć inną wersję. Dla mnie jest to najłatwiejszy sposób na wersję API w porównaniu z wersją API . Swagger jest również prostszy w konfiguracji . Ta strategia nazywa się wersjonowaniem poprzez negocjacje treści.
Dherik
3

A co powiesz na używanie dziedziczenia do przechowywania wersji modeli? Właśnie tego używam w moim projekcie i nie wymaga to specjalnej konfiguracji sprężyn i zapewnia mi dokładnie to, czego chcę.

@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);
    }
}

Taka konfiguracja pozwala na niewielkie powielanie kodu i możliwość nadpisywania metod w nowych wersjach interfejsu API przy niewielkim nakładzie pracy. Oszczędza również potrzebę komplikowania kodu źródłowego za pomocą logiki przełączania wersji. Jeśli nie zakodujesz punktu końcowego w wersji, domyślnie pobierze poprzednią wersję.

W porównaniu z tym, co robią inni, wydaje się to o wiele łatwiejsze. Czy jest coś, czego mi brakuje?

Ceekay
źródło
1
+1 za udostępnienie kodu. Jednak dziedziczenie ściśle to wiąże. Zamiast. Kontrolery (Test1 i Test2) powinny być tylko przejściem ... bez implementacji logiki. Cała logika powinna być w klasie usług, someService. W takim przypadku po prostu użyj prostej kompozycji i nigdy nie dziedzicz po innym kontrolerze
Dan Hunex Kwietnia
1
@ dan-hunex Wygląda na to, że Ceekay używa dziedziczenia do zarządzania różnymi wersjami interfejsu API. Jakie jest rozwiązanie, jeśli usuniesz dziedziczenie? I dlaczego w tym przykładzie problemem jest ścisła para? Z mojego punktu widzenia Test2 rozszerza Test1, ponieważ jest jego ulepszeniem (z taką samą rolą i tymi samymi obowiązkami), prawda?
jeremieca
2

Próbowałem już wersjonować mój interfejs API przy użyciu wersji URI , na przykład:

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

Jednak przy próbie wykonania tego zadania pojawiają się pewne wyzwania: jak zorganizować kod w różnych wersjach? Jak zarządzać dwiema (lub więcej) wersjami w tym samym czasie? Jaki wpływ ma usunięcie jakiejś wersji?

Najlepszą alternatywą, jaką znalazłem, nie była wersja całego interfejsu API, ale kontrola wersji na każdym punkcie końcowym . Ten wzorzec jest nazywany wersjonowaniem przy użyciu nagłówka Accept lub wersjonowania poprzez negocjację zawartości :

Takie podejście umożliwia nam wersjonowanie pojedynczej reprezentacji zasobów zamiast wersjonowania całego interfejsu API, co daje nam bardziej szczegółową kontrolę nad wersjonowaniem. Tworzy również mniejszy ślad w bazie kodu, ponieważ nie musimy rozwidlać całej aplikacji podczas tworzenia nowej wersji. Kolejną zaletą tego podejścia jest to, że nie wymaga implementowania reguł routingu URI wprowadzanych przez przechowywanie wersji za pośrednictwem ścieżki URI.

Wdrożenie na wiosnę

Najpierw utworzysz kontroler z podstawowym atrybutem produkcji, który będzie stosowany domyślnie dla każdego punktu końcowego w klasie.

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

}

Następnie utwórz możliwy scenariusz, w którym masz dwie wersje punktu końcowego do utworzenia zamówienia:

@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);
}

Gotowe! Po prostu wywołaj każdy punkt końcowy przy użyciu żądanej wersji nagłówka HTTP :

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

Lub, aby nazwać wersję drugą:

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

O twoich zmartwieniach:

A ponieważ nie wszystkie metody w API zmieniają się w tym samym wydaniu, nie chcę przechodzić do każdego z moich kontrolerów i zmieniać czegokolwiek dla procedury obsługi, która nie zmieniła się między wersjami

Jak wyjaśniono, ta strategia utrzymuje każdy kontroler i punkt końcowy w jego aktualnej wersji. Modyfikujesz tylko punkt końcowy, który ma modyfikacje i wymaga nowej wersji.

A Swagger?

Skonfigurowanie Swaggera z różnymi wersjami jest również bardzo łatwe przy użyciu tej strategii. Zobacz tę odpowiedź, aby uzyskać więcej informacji.

Dherik
źródło
1

W produktach możesz mieć negację. Więc dla method1 powiedz, produces="!...1.7"a w method2 mają pozytywne.

Produkty są również tablicą, więc dla metody method1 możesz powiedzieć produces={"...1.6","!...1.7","...1.8"}itp (zaakceptuj wszystkie oprócz 1.7)

Oczywiście nie tak idealne, jak zakresy, które masz na myśli, ale myślę, że łatwiejsze w utrzymaniu niż inne niestandardowe rzeczy, jeśli jest to coś niezwykłego w twoim systemie. Powodzenia!

Codesalsa
źródło
Dzięki Codesalsa, próbuję znaleźć sposób, który jest łatwy w utrzymaniu i nie wymaga aktualizowania każdego punktu końcowego za każdym razem, gdy musimy podnieść wersję.
Augusto
0

Możesz użyć AOP wokół przechwytywania

Rozważ mapowanie żądań, które odbiera wszystkie, /**/public_api/*aw tej metodzie nic nie robi;

@RequestMapping({
    "/**/public_api/*"
})
public void method2(Model model){
}

Po

@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


}

Jedynym ograniczeniem jest to, że wszystko musi znajdować się w tym samym kontrolerze.

W przypadku konfiguracji AOP spójrz na http://www.mkyong.com/spring/spring-aop-examples-advice/

hevi
źródło
Dzięki hevi, szukam bardziej „wiosennego” sposobu na zrobienie tego, ponieważ Spring już wybiera metodę wywołania bez używania AOP. Moim zdaniem AOP dodaje nowy poziom złożoności kodu, którego chciałbym uniknąć.
Augusto
@Augusto, Spring ma świetne wsparcie AOP. Powinieneś to wypróbować. :)
Konstantin Yovkov