Ich habe nach der Verwaltung einer REST-API-Version mit Spring 3.2.x gesucht, aber nichts gefunden, das einfach zu warten ist. Ich werde zuerst das Problem erklären, das ich habe, und dann eine Lösung ... aber ich frage mich, ob ich das Rad hier neu erfinde.
Ich möchte die Version basierend auf dem Accept-Header verwalten. Wenn eine Anforderung beispielsweise den Accept-Header enthält application/vnd.company.app-1.1+json
, möchte ich, dass Spring MVC diese an die Methode weiterleitet, die diese Version verarbeitet. Und da sich nicht alle Methoden in einer API in derselben Version ändern, möchte ich nicht zu jedem meiner Controller gehen und etwas für einen Handler ändern, der sich zwischen den Versionen nicht geändert hat. Ich möchte auch nicht die Logik haben, um herauszufinden, welche Version im Controller selbst verwendet werden soll (mithilfe von Service Locators), da Spring bereits herausfindet, welche Methode aufgerufen werden soll.
Wenn ich also eine API mit den Versionen 1.0 bis 1.8 nehme, in der ein Handler in Version 1.0 eingeführt und in Version 1.7 geändert wurde, möchte ich dies folgendermaßen behandeln. Stellen Sie sich vor, der Code befindet sich in einem Controller und es gibt Code, der die Version aus dem Header extrahieren kann. (Folgendes ist im Frühjahr ungültig)
@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;
}
Dies ist im Frühjahr nicht möglich, da die beiden Methoden dieselbe RequestMapping
Anmerkung haben und Spring nicht geladen werden kann. Die Idee ist, dass die VersionRange
Annotation einen offenen oder geschlossenen Versionsbereich definieren kann. Die erste Methode ist ab Version 1.0 bis 1.6 gültig, die zweite ab Version 1.7 (einschließlich der neuesten Version 1.8). Ich weiß, dass dieser Ansatz bricht, wenn sich jemand entscheidet, Version 99.99 zu bestehen, aber damit kann ich leben.
Da das oben Genannte nicht möglich ist, ohne ernsthaft zu überarbeiten, wie der Frühling funktioniert, habe ich darüber nachgedacht, an der Art und Weise zu basteln, wie Handler auf Anforderungen abgestimmt sind, insbesondere meine eigenen zu schreiben ProducesRequestCondition
und den Versionsbereich dort zu haben. Beispielsweise
Code:
@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;
}
Auf diese Weise kann ich geschlossene oder offene Versionsbereiche im Erzeugerteil der Annotation definieren. Ich arbeite gerade an dieser Lösung nun mit dem Problem , dass ich noch einige Kerne Spring MVC Klassen hatte zu ersetzen ( RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
und RequestMappingInfo
), die ich nicht mag, weil es zusätzliche Arbeit bedeutet , wenn ich auf eine neuere Version von Upgrade entscheiden Frühling.
Ich würde mich über Gedanken freuen ... und insbesondere über Vorschläge, dies auf einfachere und leichter zu pflegende Weise zu tun.
Bearbeiten
Kopfgeld hinzufügen. Um das Kopfgeld zu erhalten, beantworten Sie bitte die obige Frage, ohne vorzuschlagen, diese Logik im Controller selbst zu haben. Spring hat bereits eine Menge Logik, um die aufzurufende Controller-Methode auszuwählen, und ich möchte darauf zurückgreifen.
Bearbeiten 2
Ich habe den ursprünglichen POC (mit einigen Verbesserungen) in Github geteilt: https://github.com/augusto/restVersioning
quelle
produces={"application/json-1.0", "application/json-1.1"}
usw.Antworten:
Unabhängig davon, ob die Versionsverwaltung durch abwärtskompatible Änderungen vermieden werden kann (was möglicherweise nicht immer möglich ist, wenn Sie an einige Unternehmensrichtlinien gebunden sind oder Ihre API-Clients fehlerhaft implementiert sind und die Gewinnschwelle erreichen würden, wenn dies nicht der Fall sein sollte), ist die abstrahierte Anforderung interessant einer:
Wie kann ich eine benutzerdefinierte Anforderungszuordnung durchführen, die willkürliche Auswertungen von Headerwerten aus der Anforderung durchführt, ohne die Auswertung im Methodenkörper durchzuführen?
Wie in dieser SO-Antwort beschrieben, können Sie tatsächlich dieselbe haben
@RequestMapping
und eine andere Anmerkung verwenden, um während des tatsächlichen Routings zu unterscheiden, das zur Laufzeit stattfindet. Dazu müssen Sie:VersionRange
.RequestCondition<VersionRange>
. Da Sie so etwas wie einen Best-Match-Algorithmus haben, müssen Sie prüfen, ob Methoden, die mit anderenVersionRange
Werten versehen sind, besser zu der aktuellen Anforderung passen.VersionRangeRequestMappingHandlerMapping
basierend auf der Annotations- und Anforderungsbedingung (wie im Beitrag So implementieren Sie benutzerdefinierte @ RequestMapping-Eigenschaften beschrieben ).VersionRangeRequestMappingHandlerMapping
bevor Sie die Standardeinstellung verwendenRequestMappingHandlerMapping
(z. B. indem Sie die Reihenfolge auf 0 setzen).Dies würde keinen hackigen Austausch von Spring-Komponenten erfordern, sondern verwendet die Spring-Konfigurations- und Erweiterungsmechanismen, sodass dies auch dann funktionieren sollte, wenn Sie Ihre Spring-Version aktualisieren (sofern die neue Version diese Mechanismen unterstützt).
quelle
mvc:annotation-driven
. Hoffentlich wird Spring eine Version bereitstellen,mvc:annotation-driven
in der man benutzerdefinierte Bedingungen definieren kann.Ich habe gerade eine benutzerdefinierte Lösung erstellt. Ich verwende die
@ApiVersion
Annotation in Kombination mit@RequestMapping
Annotation innerhalb von@Controller
Klassen.Beispiel:
Implementierung:
Anmerkung zu ApiVersion.java :
ApiVersionRequestMappingHandlerMapping.java (dies ist meistens Kopieren und Einfügen von
RequestMappingHandlerMapping
):Injektion in WebMvcConfigurationSupport:
quelle
/v1/aResource
und/v2/aResource
sehen aus wie verschiedene Ressourcen, aber es ist nur eine andere Darstellung derselben Ressource! 2. Die Verwendung von HTTP-Headern sieht besser aus, aber Sie können niemandem eine URL geben, da die URL den Header nicht enthält. 3. Verwenden Sie einen URL-Parameter, dh/aResource?v=2.1
(übrigens: So führt Google die Versionierung durch)....
Ich bin mir immer noch nicht sicher, ob ich Option 2 oder 3 wählen würde , aber ich werde 1 aus den oben genannten Gründen nie wieder verwenden .RequestMappingHandlerMapping
in Ihre injizieren möchtenWebMvcConfiguration
, sollten Sie überschreibencreateRequestMappingHandlerMapping
stattrequestMappingHandlerMapping
! Andernfalls werden Sie auf seltsame Probleme stoßen (ich hatte plötzlich Probleme mit der verzögerten Initialisierung von Hibernates aufgrund einer geschlossenen Sitzung)WebMvcConfigurationSupport
sondern zu erweiternDelegatingWebMvcConfiguration
. Dies funktionierte für mich (siehe stackoverflow.com/questions/22267191/… )Ich würde weiterhin empfehlen, URLs für die Versionierung zu verwenden, da @RequestMapping in URLs Muster und Pfadparameter unterstützt, deren Format mit regulärem Ausdruck angegeben werden könnte.
Und um Client-Upgrades (die Sie im Kommentar erwähnt haben) durchzuführen, können Sie Aliase wie "Neueste" verwenden. Oder haben Sie eine nicht versionierte Version von API, die die neueste Version verwendet (ja).
Mit Pfadparametern können Sie auch jede komplexe Versionsbehandlungslogik implementieren. Wenn Sie bereits Bereiche haben möchten, möchten Sie möglicherweise etwas früher.
Hier einige Beispiele:
Basierend auf dem letzten Ansatz können Sie tatsächlich so etwas wie das implementieren, was Sie wollen.
Beispielsweise können Sie einen Controller haben, der nur Methodenstiche mit Versionsbehandlung enthält.
In dieser Behandlung suchen Sie (unter Verwendung von Reflection / AOP / Code-Generierungsbibliotheken) in einem Spring-Service / einer Spring-Komponente oder in derselben Klasse nach Methoden mit demselben Namen / derselben Signatur und dem erforderlichen @VersionRange und rufen sie auf, indem Sie alle Parameter übergeben.
quelle
Ich habe eine Lösung implementiert, die das Problem mit der Restversionierung PERFEKT behandelt .
Allgemeines Es gibt drei Hauptansätze für die Restversionierung:
Pfad -basierte approch, in dem der Client die Version in URL definiert:
Inhaltstyp- Header, in dem der Client die Version im Accept- Header definiert:
Benutzerdefinierter Header , in dem der Client die Version in einem benutzerdefinierten Header definiert.
Das Problem beim ersten Ansatz ist, dass Sie, wenn Sie die Version ändern, beispielsweise von v1 -> v2, wahrscheinlich die v1-Ressourcen kopieren und einfügen müssen, die nicht in den v2-Pfad geändert wurden
Das Problem beim zweiten Ansatz besteht darin, dass einige Tools wie http://swagger.io/ nicht zwischen Vorgängen mit demselben Pfad, aber unterschiedlichem Inhaltstyp unterscheiden können (siehe Problem https://github.com/OAI/OpenAPI-Specification/issues/). 146 )
Die Lösung
Da ich viel mit Restdokumentationstools arbeite, bevorzuge ich den ersten Ansatz. Meine Lösung behandelt das Problem mit dem ersten Ansatz, sodass Sie den Endpunkt nicht kopieren und in die neue Version einfügen müssen.
Angenommen, wir haben Versionen v1 und v2 für den Benutzercontroller:
Die Anforderung ist, wenn ich die v1 für die Benutzerressource anfordere, muss ich die Antwort "Benutzer V1" nehmen, andernfalls muss ich die Antwort "Benutzer V2" nehmen , wenn ich die Antwort v2 , v3 usw. anfordere .
Um dies im Frühjahr zu implementieren, müssen wir das Standardverhalten von RequestMappingHandlerMapping überschreiben :
}}
Die Implementierung liest die Version in der URL und fragt ab Frühjahr die URL .Bei dieser URL existiert nicht (zum Beispiel der Client angefordert zu lösen v3 ) , dann versuchen wir , mit v2 und so oft , bis wir den finden die aktuellste Version für die Ressource .
Nehmen wir an, wir haben zwei Ressourcen, um die Vorteile dieser Implementierung zu sehen: Benutzer und Unternehmen:
Nehmen wir an, wir haben den "Vertrag" des Unternehmens geändert, der den Kunden bricht. Also implementieren wir das
http://localhost:9001/api/v2/company
und bitten den Client, stattdessen auf v2 zu v2 zu wechseln.Die neuen Anfragen vom Kunden sind also:
anstatt:
Das Beste daran ist, dass der Client mit dieser Lösung die Benutzerinformationen aus Version 1 und die Unternehmensinformationen aus Version 2 erhält, ohne dass ein neuer (gleicher) Endpunkt aus Benutzer Version 2 erstellt werden muss!
Restdokumentation Wie ich bereits sagte, ist der Grund, warum ich mich für den URL-basierten Versionsansatz entschieden habe, dass einige Tools wie swagger die Endpunkte mit derselben URL, aber unterschiedlichem Inhaltstyp nicht unterschiedlich dokumentieren. Bei dieser Lösung werden beide Endpunkte angezeigt, da sie unterschiedliche URLs haben:
GIT
Lösungsimplementierung unter: https://github.com/mspapant/restVersioningExample/
quelle
Die
@RequestMapping
Anmerkung unterstützt einheaders
Element, mit dem Sie die übereinstimmenden Anforderungen eingrenzen können. Insbesondere können SieAccept
hier den Header verwenden.Dies ist nicht genau das, was Sie beschreiben, da es nicht direkt Bereiche behandelt, aber das Element unterstützt den Platzhalter * sowie! =. Zumindest könnten Sie damit durchkommen, einen Platzhalter für Fälle zu verwenden, in denen alle Versionen den betreffenden Endpunkt unterstützen, oder sogar alle Nebenversionen einer bestimmten Hauptversion (z. B. 1. *).
Ich glaube nicht, dass ich dieses Element schon einmal verwendet habe (wenn ich es habe, erinnere ich mich nicht), also gehe ich einfach von der Dokumentation auf
http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html
quelle
application/*
und nicht für Teile des Typs. Zum Beispiel ist das Folgende im Frühjahr ungültig"Accept=application/vnd.company.app-1.*+json"
. Dies hängt damit zusammen, wie die FrühlingsklasseMediaType
funktioniertWas ist mit der Verwendung der Vererbung zur Modellversionierung? Das ist es, was ich in meinem Projekt verwende und es erfordert keine spezielle Federkonfiguration und bringt mir genau das, was ich will.
Diese Einrichtung ermöglicht eine geringe Duplizierung des Codes und die Möglichkeit, Methoden mit wenig Aufwand in neue Versionen der API zu überschreiben. Außerdem müssen Sie Ihren Quellcode nicht mit der Versionsumschaltlogik komplizieren. Wenn Sie keinen Endpunkt in einer Version codieren, wird standardmäßig die vorherige Version abgerufen.
Im Vergleich zu dem, was andere tun, scheint dies viel einfacher zu sein. Fehlt mir etwas?
quelle
Ich habe bereits versucht, meine API mithilfe der URI-Versionierung zu versionieren .
Es gibt jedoch einige Herausforderungen, wenn Sie versuchen, dies zum Laufen zu bringen: Wie organisieren Sie Ihren Code mit verschiedenen Versionen? Wie können zwei (oder mehr) Versionen gleichzeitig verwaltet werden? Welche Auswirkungen hat das Entfernen einer Version?
Die beste Alternative, die ich gefunden habe, war nicht die Version der gesamten API, sondern die Kontrolle der Version auf jedem Endpunkt . Dieses Muster wird als Versionierung mit Accept-Header oder Versionierung durch Inhaltsverhandlung bezeichnet :
Umsetzung im Frühjahr
Zunächst erstellen Sie einen Controller mit einem grundlegenden Erzeugungsattribut, das standardmäßig für jeden Endpunkt innerhalb der Klasse gilt.
Erstellen Sie anschließend ein mögliches Szenario, in dem Sie zwei Versionen eines Endpunkts zum Erstellen einer Bestellung haben:
Getan! Rufen Sie einfach jeden Endpunkt mit der gewünschten HTTP-Header- Version auf:
Oder um die zweite Version aufzurufen:
Über Ihre Sorgen:
Wie erläutert, verwaltet diese Strategie jeden Controller und Endpunkt mit seiner aktuellen Version. Sie ändern nur den Endpunkt, der Änderungen aufweist und eine neue Version benötigt.
Und die Prahlerei?
Das Einrichten des Swagger mit verschiedenen Versionen ist mit dieser Strategie ebenfalls sehr einfach. Siehe diese Antwort für weitere Details.
quelle
In produziert kann man Negation haben. Also für Methode1 sagen
produces="!...1.7"
und in Methode2 das Positive haben.Das Erzeugnis ist auch ein Array, so dass Sie für Methode1
produces={"...1.6","!...1.7","...1.8"}
usw. sagen können (akzeptieren Sie alle außer 1.7).Natürlich nicht so ideal wie die Bereiche, die Sie sich vorgestellt haben, aber ich denke, sie sind einfacher zu warten als andere benutzerdefinierte Dinge, wenn dies in Ihrem System ungewöhnlich ist. Viel Glück!
quelle
Sie können AOP zum Abfangen verwenden
Erwägen Sie eine Anforderungszuordnung, die alle empfängt
/**/public_api/*
und bei dieser Methode nichts unternimmt.Nach dem
Die einzige Einschränkung besteht darin, dass sich alle in derselben Steuerung befinden müssen.
Informationen zur AOP-Konfiguration finden Sie unter http://www.mkyong.com/spring/spring-aop-examples-advice/.
quelle