Wie verwalte ich die REST-API-Versionierung mit Spring?

118

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 RequestMappingAnmerkung haben und Spring nicht geladen werden kann. Die Idee ist, dass die VersionRangeAnnotation 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 ProducesRequestConditionund 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, RequestMappingHandlerMappingund 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

Augusto
quelle
1
@flup Ich verstehe deinen Kommentar nicht. Das heißt nur, dass Sie Header verwenden können, und wie gesagt, was Spring sofort bereitstellt, reicht nicht aus, um APIs zu unterstützen, die ständig aktualisiert werden. Schlimmer noch, der Link zu dieser Antwort verwendet die Version in der URL.
Augusto
Vielleicht nicht genau das, wonach Sie suchen, aber Spring 3.2 unterstützt einen "Erzeuger" -Parameter in RequestMapping. Die einzige Einschränkung ist, dass die Versionsliste explizit sein muss. ZB produces={"application/json-1.0", "application/json-1.1"}usw.
Bimsapi
1
Wir müssen mehrere Versionen unserer APIs unterstützen. Diese Unterschiede sind normalerweise geringfügige Änderungen, die einige Aufrufe einiger Clients inkompatibel machen würden (es ist nicht seltsam, wenn wir 4 kleinere Versionen unterstützen müssen, in denen einige Endpunkte nicht kompatibel sind). Ich schätze den Vorschlag, ihn in die URL aufzunehmen, aber wir wissen, dass dies ein Schritt in die falsche Richtung ist, da wir einige Apps mit der Version in der URL haben und jedes Mal viel Arbeit erforderlich ist, wenn wir die URL ändern müssen Ausführung.
Augusto
1
@ Augusto, du hast es eigentlich auch nicht. Entwerfen Sie Ihre API-Änderungen einfach so, dass die Abwärtskompatibilität nicht beeinträchtigt wird. Geben Sie mir nur ein Beispiel für Änderungen, die die Kompatibilität beeinträchtigen, und ich zeige Ihnen, wie Sie diese Änderungen ohne Unterbrechung vornehmen können.
Alexey Andreev

Antworten:

62

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 @RequestMappingund eine andere Anmerkung verwenden, um während des tatsächlichen Routings zu unterscheiden, das zur Laufzeit stattfindet. Dazu müssen Sie:

  1. Erstellen Sie eine neue Anmerkung VersionRange.
  2. Implementieren Sie a RequestCondition<VersionRange>. Da Sie so etwas wie einen Best-Match-Algorithmus haben, müssen Sie prüfen, ob Methoden, die mit anderen VersionRangeWerten versehen sind, besser zu der aktuellen Anforderung passen.
  3. Implementieren Sie eine VersionRangeRequestMappingHandlerMappingbasierend auf der Annotations- und Anforderungsbedingung (wie im Beitrag So implementieren Sie benutzerdefinierte @ RequestMapping-Eigenschaften beschrieben ).
  4. Konfigurieren Sie spring so, dass Ihre Bewertung erfolgt, VersionRangeRequestMappingHandlerMappingbevor Sie die Standardeinstellung verwenden RequestMappingHandlerMapping(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).

xwoker
quelle
Vielen Dank für das Hinzufügen Ihres Kommentars als Antwort xwoker. Bis jetzt ist das beste. Ich habe die Lösung basierend auf den von Ihnen erwähnten Links implementiert und es ist nicht so schlimm. Das größte Problem tritt beim Upgrade auf eine neue Version von Spring auf, da alle Änderungen an der dahinter stehenden Logik überprüft werden müssen mvc:annotation-driven. Hoffentlich wird Spring eine Version bereitstellen, mvc:annotation-drivenin der man benutzerdefinierte Bedingungen definieren kann.
Augusto
@ Augusto, ein halbes Jahr später, wie hat das für dich geklappt? Ich bin auch neugierig, ob Sie wirklich eine Methode pro Methode verwenden. An dieser Stelle frage ich mich, ob es nicht klarer wäre, eine Version auf Klassen- / Controller-Granularität zu erstellen.
Sander Verhagen
1
@SanderVerhagen es funktioniert, aber wir versionieren die gesamte API, nicht pro Methode oder Controller (die API ist recht klein, da sie sich auf einen Aspekt des Geschäfts konzentriert). Wir haben ein erheblich größeres Projekt, bei dem eine andere Version pro Ressource verwendet und in der URL angegeben wurde (sodass Sie einen Endpunkt in / v1 / session und eine andere Ressource in einer völlig anderen Version haben können, z. B. / v4 / orders). ... es ist etwas flexibler, aber es setzt die Clients stärker unter Druck, zu wissen, welche Version von jedem Endpunkt aufgerufen werden soll.
Augusto
1
Leider funktioniert dies mit Swagger nicht gut, da beim Erweitern von WebMvcConfigurationSupport viele automatische Konfigurationen deaktiviert sind.
Rick
Ich habe diese Lösung ausprobiert, aber sie funktioniert tatsächlich nicht mit 2.3.2.RELEASE. Haben Sie ein Beispielprojekt zu zeigen?
Patrick
53

Ich habe gerade eine benutzerdefinierte Lösung erstellt. Ich verwende die @ApiVersionAnnotation in Kombination mit @RequestMappingAnnotation innerhalb von @ControllerKlassen.

Beispiel:

@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

}

Implementierung:

Anmerkung zu ApiVersion.java :

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

ApiVersionRequestMappingHandlerMapping.java (dies ist meistens Kopieren und Einfügen von 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);
    }

}

Injektion in WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}
Benjamin M.
quelle
4
Ich habe das int [] in String [] geändert, um Versionen wie "1.2" zuzulassen, und so kann ich Schlüsselwörter wie "latest"
Maelig
3
Ja, das ist ziemlich vernünftig. Für zukünftige Projekte würde ich aus verschiedenen Gründen einen anderen Weg gehen: 1. URLs repräsentieren Ressourcen. /v1/aResourceund /v2/aResourcesehen 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 .
Benjamin M.
5
Wenn Sie Ihre eigenen RequestMappingHandlerMappingin Ihre injizieren möchten WebMvcConfiguration, sollten Sie überschreiben createRequestMappingHandlerMappingstatt requestMappingHandlerMapping! Andernfalls werden Sie auf seltsame Probleme stoßen (ich hatte plötzlich Probleme mit der verzögerten Initialisierung von Hibernates aufgrund einer geschlossenen Sitzung)
stuXnet
1
Der Ansatz sieht gut aus, scheint aber irgendwie nicht mit Junta-Testfällen (SpringRunner) zu funktionieren. Jede Chance, dass Sie den Ansatz haben, mit Testfällen zu arbeiten
JDev
1
Es gibt eine Möglichkeit, diese Arbeit zu machen, nicht zu erweitern, WebMvcConfigurationSupport sondern zu erweitern DelegatingWebMvcConfiguration. Dies funktionierte für mich (siehe stackoverflow.com/questions/22267191/… )
SeB.Fr
17

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:

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

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.

schwer fassbarer Code
quelle
14

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:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Inhaltstyp- Header, in dem der Client die Version im Accept- Header definiert:

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • 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:

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

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 .

Geben Sie hier die Bildbeschreibung ein

Um dies im Frühjahr zu implementieren, müssen wir das Standardverhalten von RequestMappingHandlerMapping überschreiben :

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

}}

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:

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

Nehmen wir an, wir haben den "Vertrag" des Unternehmens geändert, der den Kunden bricht. Also implementieren wir das http://localhost:9001/api/v2/companyund bitten den Client, stattdessen auf v2 zu v2 zu wechseln.

Die neuen Anfragen vom Kunden sind also:

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

anstatt:

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

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:

Geben Sie hier die Bildbeschreibung ein

GIT

Lösungsimplementierung unter: https://github.com/mspapant/restVersioningExample/

mspapant
quelle
9

Die @RequestMappingAnmerkung unterstützt ein headersElement, mit dem Sie die übereinstimmenden Anforderungen eingrenzen können. Insbesondere können Sie Accepthier den Header verwenden.

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

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
2
Ich weiß davon, aber wie Sie bemerkt haben, müsste ich bei jeder Version zu allen meinen Controllern gehen und eine Version hinzufügen, auch wenn sie sich nicht geändert haben. Der von Ihnen erwähnte Bereich funktioniert beispielsweise nur für den vollständigen Typ 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ühlingsklasse MediaTypefunktioniert
Augusto
@ August müssen Sie dies nicht unbedingt tun. Mit diesem Ansatz versionieren Sie nicht die "API", sondern "Endpoint". Jeder Endpunkt kann eine andere Version haben. Für mich ist es der einfachste Weg, die API im Vergleich zur API-Version zu versionieren . Swagger ist auch einfacher einzurichten . Diese Strategie wird als Versionierung durch Inhaltsverhandlung bezeichnet.
Dherik
3

Was 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.

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

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?

Ceekay
quelle
1
+1 für die Freigabe des Codes. Die Vererbung koppelt es jedoch eng. Stattdessen. Die Controller (Test1 und Test2) sollten nur ein Durchgang sein ... keine logische Implementierung. Alle Logik sollte in der Serviceklasse someService sein. Verwenden Sie in diesem Fall einfach eine einfache Komposition und erben Sie niemals vom anderen Controller
Dan Hunex,
1
@ dan-hunex Es scheint, dass Ceekay die Vererbung verwendet, um die verschiedenen Versionen der API zu verwalten. Was ist die Lösung, wenn Sie die Vererbung entfernen? Und warum ist ein enges Paar in diesem Beispiel ein Problem? Aus meiner Sicht erweitert Test2 Test1, weil es eine Verbesserung darstellt (mit derselben Rolle und derselben Verantwortung), nicht wahr?
Jeremyieca
2

Ich habe bereits versucht, meine API mithilfe der URI-Versionierung zu versionieren .

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

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 :

Dieser Ansatz ermöglicht es uns, eine einzelne Ressourcendarstellung zu versionieren, anstatt die gesamte API zu versionieren, wodurch wir eine detailliertere Kontrolle über die Versionierung erhalten. Außerdem wird der Platzbedarf in der Codebasis verringert, da beim Erstellen einer neuen Version nicht die gesamte Anwendung gegabelt werden muss. Ein weiterer Vorteil dieses Ansatzes besteht darin, dass keine URI-Routing-Regeln implementiert werden müssen, die durch die Versionierung über den URI-Pfad eingeführt wurden.

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.

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

}

Erstellen Sie anschließend ein mögliches Szenario, in dem Sie zwei Versionen eines Endpunkts zum Erstellen einer Bestellung haben:

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

Getan! Rufen Sie einfach jeden Endpunkt mit der gewünschten HTTP-Header- Version auf:

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

Oder um die zweite Version aufzurufen:

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

Über Ihre Sorgen:

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

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.

Dherik
quelle
1

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!

Codesalsa
quelle
Dank Codesalsa versuche ich, einen Weg zu finden, der einfach zu warten ist und nicht erforderlich ist, um jeden Endpunkt jedes Mal zu aktualisieren, wenn wir die Version anstoßen müssen.
Augusto
0

Sie können AOP zum Abfangen verwenden

Erwägen Sie eine Anforderungszuordnung, die alle empfängt /**/public_api/*und bei dieser Methode nichts unternimmt.

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

Nach dem

@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


}

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/.

hevi
quelle
Vielen Dank, hevi, ich suche nach einer "frühlingsfreundlicheren" Methode, da Spring bereits auswählt, welche Methode ohne Verwendung von AOP aufgerufen werden soll. Ich bin der Ansicht, dass AOP eine neue Ebene der Codekomplexität hinzufügt, die ich vermeiden möchte.
Augusto
@Augusto, Spring hat eine großartige AOP-Unterstützung. Sie sollten es ausprobieren. :)
Konstantin Yovkov