Come gestire il controllo delle versioni dell'API REST con la primavera?


119

Ho cercato come gestire le versioni di un'API REST utilizzando Spring 3.2.x, ma non ho trovato nulla di facile da mantenere. Spiegherò prima il problema che ho e poi una soluzione ... ma mi chiedo se sto reinventando la ruota qui.

Voglio gestire la versione in base all'intestazione Accept e, ad esempio, se una richiesta ha l'intestazione Accept application/vnd.company.app-1.1+json, voglio che Spring MVC la inoltri al metodo che gestisce questa versione. E poiché non tutti i metodi in un'API cambiano nella stessa versione, non voglio andare su ciascuno dei miei controller e cambiare qualcosa per un gestore che non è cambiato tra le versioni. Inoltre, non voglio avere la logica per capire quale versione usare nel controller stesso (usando i localizzatori di servizi) poiché Spring sta già scoprendo quale metodo chiamare.

Quindi, presa un'API con le versioni 1.0, alla 1.8 dove un gestore è stato introdotto nella versione 1.0 e modificato nella v1.7, vorrei gestirlo nel modo seguente. Immagina che il codice sia all'interno di un controller e che ci sia del codice in grado di estrarre la versione dall'intestazione. (Quanto segue non è valido in primavera)

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

Questo non è possibile in primavera poiché i 2 metodi hanno la stessa RequestMappingannotazione e Spring non si carica. L'idea è che l' VersionRangeannotazione possa definire un intervallo di versioni aperto o chiuso. Il primo metodo è valido dalle versioni 1.0 alla 1.6, mentre il secondo per la versione 1.7 in poi (inclusa l'ultima versione 1.8). So che questo approccio si interrompe se qualcuno decide di passare la versione 99.99, ma è qualcosa con cui posso convivere.

Ora, poiché quanto sopra non è possibile senza una seria rielaborazione di come funziona la primavera, stavo pensando di armeggiare con il modo in cui i gestori corrispondevano alle richieste, in particolare per scrivere la mia ProducesRequestCondition, e avere la gamma di versioni lì dentro. Per esempio

Codice:

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

In questo modo, posso avere intervalli di versioni chiusi o aperti definiti nella parte produce dell'annotazione. Sto lavorando a questa soluzione ora, con il problema che avevo ancora a sostituire alcune classi nucleo Spring MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappinge RequestMappingInfo), che non mi piace, perché vuol dire lavoro extra ogni volta che decido di effettuare l'aggiornamento a una nuova versione di primavera.

Apprezzerei qualsiasi pensiero ... e soprattutto, qualsiasi suggerimento per farlo in un modo più semplice e facile da mantenere.


modificare

Aggiunta di una taglia. Per ottenere la taglia, rispondi alla domanda sopra senza suggerire di avere questa logica nel controller stesso. Spring ha già molta logica per selezionare il metodo del controller da chiamare, e voglio tornare su questo.


Modifica 2

Ho condiviso il POC originale (con alcuni miglioramenti) in github: https://github.com/augusto/restVersioning



1
@flup non capisco il tuo commento. Questo dice solo che puoi usare le intestazioni e, come ho detto, ciò che la molla fornisce fuori dagli schemi non è sufficiente per supportare le API che vengono aggiornate costantemente. Ancora peggio, il collegamento a quella risposta utilizza la versione nell'URL.
Augusto

Forse non è esattamente quello che stai cercando, ma Spring 3.2 supporta un parametro "produce" su RequestMapping. L'unico avvertimento è che l'elenco delle versioni deve essere esplicito. Ad esempio, produces={"application/json-1.0", "application/json-1.1"}ecc
bimsapi

1
Abbiamo bisogno di supportare diverse versioni delle nostre API, queste differenze sono solitamente piccole modifiche che renderebbero incompatibili alcune chiamate da alcuni client (non sarebbe strano se avessimo bisogno di supportare 4 versioni minori, in cui alcuni endpoint sono incompatibili). Apprezzo il suggerimento di inserirlo nell'URL, ma sappiamo che è un passo nella direzione sbagliata, poiché abbiamo un paio di app con la versione nell'URL e c'è molto lavoro da fare ogni volta che dobbiamo eseguire il bump versione.
Augusto

1
@Augusto, in realtà non l'hai fatto anche tu. Basta progettare le modifiche dell'API in modo da non interrompere la compatibilità con le versioni precedenti. Fammi solo un esempio di modifiche che rompono la compatibilità e ti mostro come apportare queste modifiche in modo senza interruzioni.
Alexey Andreev

Risposte:


62

Indipendentemente dal fatto che il controllo delle versioni possa essere evitato apportando modifiche compatibili con le versioni precedenti (cosa che potrebbe non essere sempre possibile quando si è vincolati da alcune linee guida aziendali o i client API sono implementati in modo errato e si interrompono anche se non dovrebbero), il requisito astratto è interessante uno:

Come posso eseguire una mappatura della richiesta personalizzata che esegue valutazioni arbitrarie dei valori di intestazione dalla richiesta senza eseguire la valutazione nel corpo del metodo?

Come descritto in questa risposta SO, puoi effettivamente avere lo stesso @RequestMappinge utilizzare un'annotazione diversa per differenziare durante il routing effettivo che avviene durante il runtime. Per fare ciò, dovrai:

  1. Crea una nuova annotazione VersionRange.
  2. Implementa a RequestCondition<VersionRange>. Dal momento che avrai qualcosa come un algoritmo di corrispondenza migliore, dovrai verificare se i metodi annotati con altri VersionRangevalori forniscono una corrispondenza migliore per la richiesta corrente.
  3. Implementare una condizione VersionRangeRequestMappingHandlerMappingbasata sull'annotazione e sulla richiesta (come descritto nel post Come implementare le proprietà personalizzate @RequestMapping ).
  4. Configura la molla per valutare la tua VersionRangeRequestMappingHandlerMappingprima di utilizzare il valore predefinito RequestMappingHandlerMapping(ad esempio impostando il suo ordine a 0).

Ciò non richiederebbe sostituzioni hacker dei componenti di Spring, ma utilizza la configurazione di Spring e i meccanismi di estensione, quindi dovrebbe funzionare anche se aggiorni la tua versione di Spring (purché la nuova versione supporti questi meccanismi).


Grazie per aver aggiunto il tuo commento come risposta xwoker. Fino ad ora è il migliore. Ho implementato la soluzione in base ai link che hai menzionato e non è poi così male. Il problema più grande si manifesterà durante l'aggiornamento a una nuova versione di Spring in quanto richiederà di verificare eventuali modifiche alla logica sottostante mvc:annotation-driven. Si spera che Spring fornisca una versione mvc:annotation-drivenin cui si possono definire condizioni personalizzate.
Augusto

@Augusto, sei mesi dopo, come ti è andata? Inoltre, sono curioso, stai davvero controllando le versioni in base al metodo? A questo punto mi chiedo se non sarebbe più chiaro la versione con granularità a livello di classe / per controller?
Sander Verhagen

1
@SanderVerhagen funziona, ma eseguiamo la versione dell'intera API, non per metodo o controller (l'API è piuttosto piccola in quanto è focalizzata su un aspetto del business). Abbiamo un progetto notevolmente più grande in cui hanno scelto di utilizzare una versione diversa per risorsa e specificarla sull'URL (in modo da poter avere un endpoint su / v1 / sessioni e un'altra risorsa su una versione completamente diversa, ad esempio / v4 / orders) ... è un po 'più flessibile, ma mette più pressione sui client per sapere quale versione chiamare di ogni endpoint.
Augusto

1
Sfortunatamente, questo non funziona bene con Swagger, poiché molte configurazioni automatiche vengono disattivate quando si estende WebMvcConfigurationSupport.
Rick

Ho provato questa soluzione ma in realtà non funziona con 2.3.2.RELEASE. Hai qualche progetto di esempio da mostrare?
Patrick

54

Ho appena creato una soluzione personalizzata. Sto usando l' @ApiVersionannotazione in combinazione con l' @RequestMappingannotazione all'interno delle @Controllerclassi.

Esempio:

@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

}

Implementazione:

Annotazione ApiVersion.java :

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

ApiVersionRequestMappingHandlerMapping.java (questo è principalmente copia e incolla da 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);
    }

}

Iniezione in WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}

4
Ho cambiato int [] in String [] per consentire versioni come "1.2", quindi posso gestire parole chiave come "latest"
Maelig

3
Sì, è abbastanza ragionevole. Per i progetti futuri, sceglierei una strada diversa per alcuni motivi: 1. Gli URL rappresentano le risorse. /v1/aResourcee /v2/aResourcesembrano risorse diverse, ma è solo una rappresentazione diversa della stessa risorsa! 2. L' utilizzo delle intestazioni HTTP sembra migliore, ma non è possibile fornire a qualcuno un URL, perché l'URL non contiene l'intestazione. 3. Utilizzando un parametro URL, ad esempio /aResource?v=2.1(btw: questo è il modo in cui Google esegue il controllo delle versioni). ...Non sono ancora sicuro se sceglierei l'opzione 2 o 3 , ma non userò mai più 1 per i motivi sopra menzionati.
Benjamin M

5
Se vuoi iniettare il tuo RequestMappingHandlerMappingnel tuo WebMvcConfiguration, dovresti sovrascriverlo createRequestMappingHandlerMappinginvece di requestMappingHandlerMapping! Altrimenti incontrerai strani problemi (improvvisamente ho avuto problemi con l'inizializzazione pigra di Hibernates a causa di una sessione chiusa)
stuXnet

1
L'approccio sembra buono ma in qualche modo sembra che non funzioni con i casi di test junti (SpringRunner). Qualsiasi possibilità che tu abbia l'approccio funzionante con casi di test
JDev

1
C'è un modo per far funzionare questo, non estendere WebMvcConfigurationSupport ma estendere DelegatingWebMvcConfiguration. Questo ha funzionato per me (vedi stackoverflow.com/questions/22267191/... )
SeB.Fr

17

Consiglierei comunque di utilizzare gli URL per il controllo delle versioni perché negli URL @RequestMapping supporta pattern e parametri di percorso, il cui formato potrebbe essere specificato con regexp.

E per gestire gli aggiornamenti del client (che hai menzionato nel commento) puoi usare alias come "latest". Oppure hai una versione non aggiornata di API che utilizza l'ultima versione (sì).

Inoltre, utilizzando i parametri di percorso è possibile implementare qualsiasi logica di gestione delle versioni complessa e, se si desidera già disporre di intervalli, è molto probabile che si desideri qualcosa di più abbastanza presto.

Ecco un paio di esempi:

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

Sulla base dell'ultimo approccio puoi effettivamente implementare qualcosa di simile a ciò che desideri.

Ad esempio, puoi avere un controller che contiene solo metodi stab con la gestione della versione.

In quella gestione si guarda (usando la reflection / AOP / le librerie di generazione del codice) in qualche servizio / componente primaverile o nella stessa classe per il metodo con lo stesso nome / firma e @VersionRange richiesto e lo si richiama passando tutti i parametri.


14

Ho implementato una soluzione che gestisce PERFETTAMENTE il problema con il rest versioning.

Parlando in generale ci sono 3 approcci principali per il rest versioning:

  • Approccio basato sul percorso , in cui il client definisce la versione in URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Intestazione Content-Type , in cui il client definisce la versione nell'intestazione Accept :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Intestazione personalizzata , in cui il client definisce la versione in un'intestazione personalizzata.

Il problema con il primo approccio è che se modifichi la versione diciamo da v1 -> v2, probabilmente devi copiare e incollare le risorse v1 che non sono cambiate nel percorso v2

Il problema con il secondo approccio è che alcuni strumenti come http://swagger.io/ non possono distinguere tra operazioni con lo stesso percorso ma diverso tipo di contenuto (controllare il problema https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

La soluzione

Dato che sto lavorando molto con gli strumenti di documentazione rest, preferisco utilizzare il primo approccio. La mia soluzione gestisce il problema con il primo approccio, quindi non è necessario copiare e incollare l'endpoint nella nuova versione.

Supponiamo di avere le versioni v1 e v2 per il controller utente:

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

Il requisito è che se richiedo la v1 per la risorsa utente devo prendere la risposta "Utente V1" , altrimenti se richiedo la v2 , v3 e così via devo accettare la risposta "Utente V2" .

inserisci qui la descrizione dell'immagine

Per implementarlo in primavera, dobbiamo sovrascrivere il comportamento predefinito 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 ;
    }

}

L'implementazione legge la versione nell'URL e chiede dalla primavera di risolvere l'URL. Nel caso in cui questo URL non esista (ad esempio il client ha richiesto v3 ) quindi proviamo con v2 e così uno fino a trovare la versione più recente per la risorsa .

Per vedere i vantaggi di questa implementazione, supponiamo di avere due risorse: Utente e Azienda:

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

Diciamo che abbiamo apportato una modifica al "contratto" aziendale che infrange il cliente. Quindi implementiamo http://localhost:9001/api/v2/companye chiediamo al client di passare a v2 invece di v1.

Quindi le nuove richieste dal cliente sono:

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

invece di:

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

La parte migliore qui è che con questa soluzione il client otterrà le informazioni sull'utente dalla v1 e le informazioni sulla società dalla v2 senza la necessità di creare un nuovo (stesso) endpoint dall'utente v2!

Rest Documentation Come ho detto prima, il motivo per cui scelgo l'approccio di versioning basato su URL è che alcuni strumenti come swagger non documentano in modo diverso gli endpoint con lo stesso URL ma un tipo di contenuto diverso. Con questa soluzione, vengono visualizzati entrambi gli endpoint poiché hanno un URL diverso:

inserisci qui la descrizione dell'immagine

IDIOTA

Implementazione della soluzione su: https://github.com/mspapant/restVersioningExample/


9

L' @RequestMappingannotazione supporta un headerselemento che consente di restringere le richieste corrispondenti. In particolare puoi usare l' Acceptintestazione qui.

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

Questo non è esattamente ciò che stai descrivendo, poiché non gestisce direttamente gli intervalli, ma l'elemento supporta il carattere jolly * e! =. Quindi almeno potresti cavartela usando un carattere jolly per i casi in cui tutte le versioni supportano l'endpoint in questione, o anche tutte le versioni secondarie di una data versione principale (ad esempio 1. *).

Non penso di aver effettivamente usato questo elemento prima (se l'ho fatto non ricordo), quindi sto solo uscendo dalla documentazione su

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


2
Lo so, ma come hai notato, su ogni versione dovrei andare su tutti i miei controller e aggiungere una versione, anche se non sono cambiati. L'intervallo che hai citato funziona solo sul tipo completo, ad esempio application/*e non su parti del tipo. Ad esempio, quanto segue non è valido in primavera "Accept=application/vnd.company.app-1.*+json". Questo è legato alla primavera come la classe MediaTypeopere
Augusto

@Augusto non devi necessariamente farlo. Con questo approccio, non si esegue il controllo delle versioni dell '"API" ma di "Endpoint". Ogni endpoint può avere una versione diversa. Per me è il modo più semplice per eseguire la versione dell'API rispetto alla versione dell'API . Swagger è anche più semplice da configurare . Questa strategia è denominata Controllo delle versioni tramite negoziazione del contenuto.
Dherik

3

Che dire dell'utilizzo dell'ereditarietà per il controllo delle versioni del modello? Questo è ciò che sto usando nel mio progetto e non richiede una configurazione speciale della molla e mi dà esattamente quello che voglio.

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

Questa configurazione consente una piccola duplicazione del codice e la possibilità di sovrascrivere i metodi nelle nuove versioni dell'API con poco lavoro. Inoltre, evita di complicare il codice sorgente con la logica di cambio versione. Se non si codifica un endpoint in una versione, per impostazione predefinita verrà acquisita la versione precedente.

Rispetto a quello che fanno gli altri, questo sembra molto più semplice. C'è qualcosa che mi manca?


1
+1 per la condivisione del codice. Tuttavia, l'eredità lo accoppia strettamente. Anziché. i controller (Test1 e Test2) dovrebbero essere solo un passaggio ... nessuna implementazione logica. Tutta la logica dovrebbe essere nella classe di servizio, someService. In tal caso, usa semplicemente la composizione semplice e non ereditare mai dall'altro controller
Dan Hunex

1
@ dan-hunex Sembra che Ceekay utilizzi l'ereditarietà per gestire le diverse versioni dell'api. Se rimuovi l'ereditarietà, qual è la soluzione? E perché la coppia stretta è un problema in questo esempio? Dal mio punto di vista, Test2 estende Test1 perché ne è un miglioramento (con lo stesso ruolo e le stesse responsabilità), no?
jeremieca

2

Ho già provato a eseguire la versione della mia API utilizzando il controllo delle versioni dell'URI , come:

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

Ma ci sono alcune sfide quando si cerca di farlo funzionare: come organizzare il codice con versioni diverse? Come gestire due (o più) versioni contemporaneamente? Qual è l'impatto quando si rimuove qualche versione?

La migliore alternativa che ho trovato non è stata la versione dell'intera API, ma il controllo della versione su ogni endpoint . Questo modello è denominato Controllo delle versioni utilizzando l'intestazione Accetta o Controllo delle versioni tramite negoziazione del contenuto :

Questo approccio ci consente di eseguire la versione di una singola rappresentazione della risorsa invece di eseguire il controllo delle versioni dell'intera API, il che ci offre un controllo più granulare sul controllo delle versioni. Crea anche un'impronta più piccola nella base del codice poiché non è necessario eseguire il fork dell'intera applicazione durante la creazione di una nuova versione. Un altro vantaggio di questo approccio è che non richiede l'implementazione delle regole di instradamento URI introdotte dal controllo delle versioni tramite il percorso URI.

Attuazione in primavera

Innanzitutto, crei un controller con un attributo produce di base, che verrà applicato per impostazione predefinita a ciascun endpoint all'interno della classe.

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

}

Successivamente, crea un possibile scenario in cui hai due versioni di un endpoint per creare un ordine:

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

Fatto! Basta chiamare ogni endpoint utilizzando la versione dell'intestazione HTTP desiderata :

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

Oppure, per chiamare la versione due:

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

Sulle tue preoccupazioni:

E poiché non tutti i metodi in un'API cambiano nella stessa versione, non voglio andare su ciascuno dei miei controller e cambiare nulla per un gestore che non è cambiato tra le versioni

Come spiegato, questa strategia mantiene ogni controller ed endpoint con la sua versione attuale. Si modifica solo l'endpoint che ha modifiche e necessita di una nuova versione.

E lo Swagger?

Anche configurare Swagger con versioni diverse è molto semplice utilizzando questa strategia. Vedi questa risposta per maggiori dettagli.


1

Nei prodotti puoi avere la negazione. Quindi per il metodo1 diciamoproduces="!...1.7" e nel metodo2 hai il positivo.

Anche il produce è un array, quindi per il metodo1 puoi dire produces={"...1.6","!...1.7","...1.8"}ecc. (Accetta tutto tranne 1.7)

Ovviamente non è l'ideale come le gamme che hai in mente, ma penso che sia più facile da mantenere rispetto ad altre cose personalizzate se questo è qualcosa di raro nel tuo sistema. In bocca al lupo!


Grazie codesalsa, sto cercando di trovare un modo che sia facile da mantenere e non richieda l'aggiornamento di ogni endpoint ogni volta che dobbiamo eseguire il bump della versione.
Augusto

0

Puoi usare AOP, intorno all'intercettazione

Considerare di avere una mappatura della richiesta che riceve tutto il /**/public_api/*e in questo metodo non fare nulla;

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

Dopo

@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


}

L'unico vincolo è che tutto deve essere nello stesso controller.

Per la configurazione AOP, dai un'occhiata a http://www.mkyong.com/spring/spring-aop-examples-advice/


Grazie hevi, sto cercando un modo più "primaverile" per farlo, poiché Spring seleziona già quale metodo chiamare senza utilizzare AOP. A mio avviso, AOP aggiunge un nuovo livello di complessità del codice che vorrei evitare.
Augusto

@Augusto, Spring ha un ottimo supporto per l'AOP. Dovresti provarlo. :)
Konstantin Yovkov
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.