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 RequestMapping
adnotacje, a sprężyna nie ładuje się. Chodzi o to, że VersionRange
adnotacja 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 ProducesRequestCondition
i 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
, RequestMappingHandlerMapping
i 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
źródło
produces={"application/json-1.0", "application/json-1.1"}
Itd.Odpowiedzi:
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
@RequestMapping
i użyć innej adnotacji, aby rozróżnić podczas rzeczywistego routingu, który ma miejsce w czasie wykonywania. Aby to zrobić, będziesz musiał:VersionRange
.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.VersionRangeRequestMappingHandlerMapping
podstawie adnotacji i warunku żądania (zgodnie z opisem w poście Jak zaimplementować niestandardowe właściwości @RequestMapping ).VersionRangeRequestMappingHandlerMapping
przed użyciem domyślnegoRequestMappingHandlerMapping
(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).
źródło
mvc:annotation-driven
. Miejmy nadzieję, że Spring dostarczy wersję,mvc:annotation-driven
w której można zdefiniować niestandardowe warunki.Właśnie stworzyłem niestandardowe rozwiązanie. Używam
@ApiVersion
adnotacji w połączeniu z@RequestMapping
adnotacją wewnętrzną@Controller
klas.Przykład:
Realizacja:
Adnotacja ApiVersion.java :
ApiVersionRequestMappingHandlerMapping.java (jest to głównie kopiowanie i wklejanie
RequestMappingHandlerMapping
):Wstrzyknięcie do WebMvcConfigurationSupport:
źródło
/v1/aResource
i/v2/aResource
wyglą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.RequestMappingHandlerMapping
do swojegoWebMvcConfiguration
, należy nadpisaćcreateRequestMappingHandlerMapping
zamiastrequestMappingHandlerMapping
! W przeciwnym razie napotkasz dziwne problemy (nagle miałem problemy z leniwą inicjalizacją Hibernates z powodu zamkniętej sesji)WebMvcConfigurationSupport
ale rozszerzajDelegatingWebMvcConfiguration
. To zadziałało dla mnie (patrz stackoverflow.com/questions/22267191/… )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:
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.
źródło
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:
Nagłówek Content-Type , w którym klient definiuje wersję w nagłówku Accept :
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:
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ź.
Aby zaimplementować to wiosną, musimy nadpisać domyślne zachowanie RequestMappingHandlerMapping :
}
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:
Powiedzmy, że dokonaliśmy zmiany w „kontrakcie” firmy, która łamie klienta. Dlatego implementujemy
http://localhost:9001/api/v2/company
i prosimy klienta o zmianę na v2 zamiast na v1.Tak więc nowe żądania od klienta to:
zamiast:
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:
GIT
Wdrożenie rozwiązania pod adresem : https://github.com/mspapant/restVersioningExample/
źródło
@RequestMapping
Adnotacji obsługujeheaders
element, który pozwala ograniczyć żądania ogłoszeń. W szczególności możesz tutaj użyćAccept
nagłówka.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
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, jakMediaType
działa klasa wiosennaA 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ę.
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?
źródło
Próbowałem już wersjonować mój interfejs API przy użyciu wersji URI , na przykład:
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 :
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.
Następnie utwórz możliwy scenariusz, w którym masz dwie wersje punktu końcowego do utworzenia zamówienia:
Gotowe! Po prostu wywołaj każdy punkt końcowy przy użyciu żądanej wersji nagłówka HTTP :
Lub, aby nazwać wersję drugą:
O twoich zmartwieniach:
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.
źródło
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!
źródło
Możesz użyć AOP wokół przechwytywania
Rozważ mapowanie żądań, które odbiera wszystkie,
/**/public_api/*
aw tej metodzie nic nie robi;Po
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/
źródło