Comment gérer la gestion des versions de l'API REST avec Spring?


119

J'ai cherché comment gérer les versions d'une API REST à l'aide de Spring 3.2.x, mais je n'ai rien trouvé de facile à maintenir. Je vais d'abord expliquer le problème que j'ai, puis une solution ... mais je me demande si je réinvente la roue ici.

Je veux gérer la version en fonction de l'en-tête Accept, et par exemple si une demande a l'en-tête Accept application/vnd.company.app-1.1+json, je veux que spring MVC la transmette à la méthode qui gère cette version. Et comme toutes les méthodes d'une API ne changent pas dans la même version, je ne veux pas accéder à chacun de mes contrôleurs et changer quoi que ce soit pour un gestionnaire qui n'a pas changé entre les versions. Je ne veux pas non plus avoir la logique de déterminer quelle version utiliser dans le contrôleur lui-même (en utilisant des localisateurs de service) car Spring découvre déjà la méthode à appeler.

Donc, pris une API avec les versions 1.0, à 1.8 où un gestionnaire a été introduit dans la version 1.0 et modifié dans la v1.7, je voudrais gérer cela de la manière suivante. Imaginez que le code se trouve à l'intérieur d'un contrôleur et qu'il y ait du code capable d'extraire la version de l'en-tête. (Ce qui suit n'est pas valide au printemps)

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

Cela n'est pas possible au printemps car les 2 méthodes ont la même RequestMappingannotation et Spring ne se charge pas. L'idée est que l' VersionRangeannotation peut définir une plage de versions ouverte ou fermée. La première méthode est valable des versions 1.0 à 1.6, tandis que la seconde pour la version 1.7 et suivantes (y compris la dernière version 1.8). Je sais que cette approche se rompt si quelqu'un décide de passer la version 99.99, mais c'est quelque chose avec lequel je suis d'accord.

Maintenant, comme ce qui précède n'est pas possible sans une refonte sérieuse du fonctionnement du ressort, je pensais bricoler la façon dont les gestionnaires correspondent aux demandes, en particulier pour écrire les miens ProducesRequestCondition, et y inclure la gamme de versions. Par exemple

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

De cette façon, je peux avoir des plages de versions fermées ou ouvertes définies dans la partie produit de l'annotation. Je travaille sur cette solution maintenant, avec le problème que je devais encore remplacer certaines classes de base Spring MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappinget RequestMappingInfo), ce que je n'aime pas, car cela signifie un travail supplémentaire chaque fois que je décide de passer à une version plus récente de printemps.

J'apprécierais toute réflexion ... et surtout toute suggestion de faire cela d'une manière plus simple et plus facile à maintenir.


Éditer

Ajouter une prime. Pour obtenir la prime, veuillez répondre à la question ci-dessus sans suggérer d'avoir cette logique dans le contrôleur lui-même. Spring a déjà beaucoup de logique pour sélectionner la méthode de contrôleur à appeler, et je veux utiliser cela.


Modifier 2

J'ai partagé le POC original (avec quelques améliorations) dans github: https://github.com/augusto/restVersioning



1
@flup Je ne comprends pas votre commentaire. Cela dit simplement que vous pouvez utiliser des en-têtes et, comme je l'ai dit, ce que Spring fournit prêt à l'emploi n'est pas suffisant pour prendre en charge les API qui sont constamment mises à jour. Pire encore, le lien sur cette réponse utilise la version de l'URL.
Augusto

Peut-être pas exactement ce que vous recherchez, mais Spring 3.2 prend en charge un paramètre "produit" sur RequestMapping. La seule mise en garde est que la liste des versions doit être explicite. Par exemple produces={"application/json-1.0", "application/json-1.1"}
,,

1
Nous devons prendre en charge plusieurs versions de nos API, ces différences sont généralement des modifications mineures qui rendraient certains appels de certains clients incompatibles (ce ne sera pas étrange si nous devons prendre en charge 4 versions mineures, dans lesquelles certains points de terminaison sont incompatibles). J'apprécie la suggestion de le mettre dans l'URL, mais nous savons que c'est un pas dans la mauvaise direction, car nous avons quelques applications avec la version dans l'URL et il y a beaucoup de travail à faire chaque fois que nous devons changer le version.
Augusto

1
@Augusto, vous ne l'avez pas fait aussi. Concevez simplement les modifications de votre API d'une manière qui ne rompt pas la rétrocompatibilité. Donnez-moi juste un exemple de changements qui cassent la compatibilité et je vous montre comment effectuer ces changements de manière insécable.
Alexey Andreev

Réponses:


62

Indépendamment du fait que le versionnage puisse être évité en effectuant des modifications rétrocompatibles (ce qui peut ne pas toujours être possible lorsque vous êtes lié par certaines directives d'entreprise ou que vos clients API sont implémentés de manière boguée et se casseraient même s'ils ne le devraient pas), l'exigence abstraite est intéressante une:

Comment puis-je faire un mappage de demande personnalisé qui effectue des évaluations arbitraires des valeurs d'en-tête de la demande sans effectuer l'évaluation dans le corps de la méthode?

Comme décrit dans cette réponse SO, vous pouvez en fait avoir la même chose @RequestMappinget utiliser une annotation différente pour différencier lors du routage réel qui se produit pendant l'exécution. Pour ce faire, vous devrez:

  1. Créez une nouvelle annotation VersionRange.
  2. Mettre en œuvre un RequestCondition<VersionRange>. Puisque vous aurez quelque chose comme un algorithme de meilleure correspondance, vous devrez vérifier si les méthodes annotées avec d'autres VersionRangevaleurs fournissent une meilleure correspondance pour la requête actuelle.
  3. Implémentez un VersionRangeRequestMappingHandlerMappingbasé sur l'annotation et la condition de demande (comme décrit dans l'article Comment implémenter les propriétés personnalisées @RequestMapping ).
  4. Configurez le ressort pour évaluer votre VersionRangeRequestMappingHandlerMappingavant d'utiliser la valeur RequestMappingHandlerMappingpar défaut (par exemple en définissant son ordre sur 0).

Cela ne nécessiterait aucun remplacement hacky des composants Spring, mais utilise la configuration Spring et les mécanismes d'extension, cela devrait donc fonctionner même si vous mettez à jour votre version Spring (tant que la nouvelle version prend en charge ces mécanismes).


Merci d'avoir ajouté votre commentaire comme réponse xwoker. Jusqu'à présent, c'est le meilleur. J'ai implémenté la solution en fonction des liens que vous avez mentionnés et ce n'est pas si mal. Le plus gros problème se manifestera lors de la mise à niveau vers une nouvelle version de Spring car il faudra vérifier toute modification de la logique derrière mvc:annotation-driven. Espérons que Spring fournira une version de mvc:annotation-drivendans laquelle on pourra définir des conditions personnalisées.
Augusto

@Augusto, six mois plus tard, comment cela s'est-il passé pour vous? De plus, je suis curieux de savoir si vous effectuez vraiment un versionnage par méthode? À ce stade, je me demande s'il ne serait pas plus clair de définir une version au niveau de la granularité par classe / par contrôleur?
Sander Verhagen

1
@SanderVerhagen cela fonctionne, mais nous faisons une version de l'API entière, pas par méthode ou contrôleur (l'API est assez petite car elle est concentrée sur un aspect de l'entreprise). Nous avons un projet considérablement plus grand où ils ont choisi d'utiliser une version différente par ressource et de le spécifier sur l'URL (vous pouvez donc avoir un point de terminaison sur / v1 / sessions et une autre ressource sur une version complètement différente, par exemple / v4 / orders) ... c'est un peu plus flexible, mais cela met plus de pression sur les clients pour savoir quelle version appeler de chaque point de terminaison.
Augusto

1
Malheureusement, cela ne fonctionne pas bien avec Swagger, car une grande partie de la configuration automatique est désactivée lors de l'extension de WebMvcConfigurationSupport.
Rick

J'ai essayé cette solution mais elle ne fonctionne en fait pas avec 2.3.2.RELEASE. Avez-vous un exemple de projet à montrer?
Patrick le

54

Je viens de créer une solution personnalisée. J'utilise l' @ApiVersionannotation en combinaison avec l' @RequestMappingannotation à l'intérieur des @Controllerclasses.

Exemple:

@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

}

La mise en oeuvre:

Annotation ApiVersion.java :

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

ApiVersionRequestMappingHandlerMapping.java (il s'agit principalement de copier-coller à partir de 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);
    }

}

Injection dans WebMvcConfigurationSupport:

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

4
J'ai changé l'int [] en String [] pour autoriser les versions comme "1.2", et ainsi je peux gérer des mots-clés comme "latest"
Maelig

3
Oui, c'est assez raisonnable. Pour les projets futurs, j'irais différemment pour certaines raisons: 1. Les URL représentent des ressources. /v1/aResourceet /v2/aResourceressemblent à des ressources différentes, mais c'est juste une représentation différente de la même ressource! 2. L' utilisation des en-têtes HTTP est meilleure, mais vous ne pouvez pas donner d'URL à quelqu'un, car l'URL ne contient pas l'en-tête. 3. Utilisation d'un paramètre d'URL, c'est-à-dire /aResource?v=2.1(btw: c'est ainsi que Google effectue le contrôle de version). ...Je ne sais toujours pas si j'irais avec l'option 2 ou 3 , mais je n'utiliserai plus jamais 1 pour les raisons mentionnées ci-dessus.
Benjamin M

5
Si vous voulez injecter le vôtre RequestMappingHandlerMappingdans votre WebMvcConfiguration, vous devez remplacer createRequestMappingHandlerMappingau lieu de requestMappingHandlerMapping! Sinon, vous rencontrerez des problèmes étranges (j'ai soudainement eu des problèmes avec l'initialisation paresseuse d'Hibernates à cause d'une session fermée)
stuXnet

1
L'approche semble bonne mais semble ne pas fonctionner avec les cas de test junti (SpringRunner). Toute chance que vous ayez une approche fonctionnant avec des cas de test
JDev

1
Il existe un moyen de faire ce travail, ne pas étendre WebMvcConfigurationSupport mais étendre DelegatingWebMvcConfiguration. Cela a fonctionné pour moi (voir stackoverflow.com/questions/22267191/… )
SeB.Fr

17

Je recommanderais toujours d'utiliser des URL pour le contrôle de version car dans les URL, @RequestMapping prend en charge les modèles et les paramètres de chemin, quel format pourrait être spécifié avec regexp.

Et pour gérer les mises à niveau des clients (que vous avez mentionnées dans le commentaire), vous pouvez utiliser des alias tels que 'latest'. Ou avoir une version non versionnée de l'API qui utilise la dernière version (ouais).

En utilisant également des paramètres de chemin, vous pouvez implémenter n'importe quelle logique de gestion de version complexe, et si vous voulez déjà avoir des plages, vous voudrez peut-être quelque chose plus tôt assez tôt.

Voici quelques exemples:

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

Sur la base de la dernière approche, vous pouvez réellement implémenter quelque chose comme ce que vous voulez.

Par exemple, vous pouvez avoir un contrôleur qui contient uniquement des stabs de méthode avec gestion de version.

Dans cette manipulation, vous recherchez (en utilisant des bibliothèques de réflexion / AOP / génération de code) dans un service / composant de ressort ou dans la même classe une méthode avec le même nom / signature et requis @VersionRange et l'appelez en passant tous les paramètres.


14

J'ai implémenté une solution qui gère PARFAITEMENT le problème de la gestion des versions de repos.

En général, il existe 3 approches principales pour la gestion des versions de repos:

  • Approche basée sur le chemin , dans laquelle le client définit la version dans l'URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • En- tête Content-Type , dans lequel le client définit la version dans l'en- tête Accept :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • En - tête personnalisé , dans lequel le client définit la version dans un en-tête personnalisé.

Le problème avec la première approche est que si vous changez la version, disons de v1 -> v2, vous devez probablement copier-coller les ressources v1 qui n'ont pas changé en chemin v2

Le problème avec la deuxième approche est que certains outils comme http://swagger.io/ ne peuvent pas faire la distinction entre les opérations avec le même chemin mais différents Content-Type (vérifier le problème https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

La solution

Comme je travaille beaucoup avec des outils de documentation de repos, je préfère utiliser la première approche. Ma solution gère le problème avec la première approche, vous n'avez donc pas besoin de copier-coller le point de terminaison dans la nouvelle version.

Disons que nous avons les versions v1 et v2 pour le contrôleur utilisateur:

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

La condition est que si je demande le v1 pour la ressource utilisateur, je dois prendre le repsonse "User V1" , sinon si je demande le v2 , v3 et ainsi de suite, je dois prendre la réponse "User V2" .

entrez la description de l'image ici

Afin de l'implémenter au printemps, nous devons remplacer le comportement par défaut de 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'implémentation lit la version dans l'URL et demande au printemps de résoudre l'URL. Dans le cas où cette URL n'existe pas (par exemple le client a demandé la v3 ) alors nous essayons avec la v2 et donc une jusqu'à ce que nous trouvions la version la plus récente de la ressource .

Pour voir les avantages de cette implémentation, disons que nous avons deux ressources: Utilisateur et Entreprise:

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

Disons que nous avons fait un changement de "contrat" ​​d'entreprise qui rompt le client. Nous implémentons donc le http://localhost:9001/api/v2/companyet nous demandons au client de passer à la v2 à la place sur la v1.

Ainsi, les nouvelles demandes du client sont:

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

au lieu de:

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

La meilleure partie ici est qu'avec cette solution, le client obtiendra les informations utilisateur de la v1 et les informations de l'entreprise de la v2 sans avoir besoin de créer un nouveau (même) point de terminaison à partir de l'utilisateur v2!

Rest Documentation Comme je l'ai déjà dit, la raison pour laquelle j'ai choisi l'approche de gestion des versions basée sur l'URL est que certains outils comme swagger ne documentent pas différemment les points de terminaison avec la même URL mais un type de contenu différent. Avec cette solution, les deux points de terminaison sont affichés car ont une URL différente:

entrez la description de l'image ici

GIT

Implémentation de la solution sur: https://github.com/mspapant/restVersioningExample/


9

L' @RequestMappingannotation prend en charge un headersélément qui vous permet de restreindre les demandes correspondantes. En particulier, vous pouvez utiliser l'en- Accepttête ici.

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

Ce n'est pas exactement ce que vous décrivez, car il ne gère pas directement les plages, mais l'élément prend en charge le caractère générique * ainsi que! =. Vous pourriez donc au moins vous en sortir en utilisant un caractère générique pour les cas où toutes les versions prennent en charge le point final en question, ou même toutes les versions mineures d'une version majeure donnée (par exemple 1. *).

Je ne pense pas avoir utilisé cet élément auparavant (si je ne m'en souviens pas), je vais donc simplement quitter la documentation à

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


2
Je sais à ce sujet, mais comme vous l'avez noté, sur chaque version, je devrais accéder à tous mes contrôleurs et ajouter une version, même si elles n'ont pas changé. La plage que vous avez mentionnée ne fonctionne que sur le type complet, par exemple, application/*et non sur les parties du type. Par exemple, ce qui suit n'est pas valide au printemps "Accept=application/vnd.company.app-1.*+json". Ceci est lié à la façon dont la classe printemps MediaTypeœuvres
Augusto

@Augusto vous n'avez pas nécessairement besoin de faire cela. Avec cette approche, vous ne versez pas "l'API" mais "Endpoint". Chaque point de terminaison peut avoir une version différente. Pour moi, c'est le moyen le plus simple de version de l'API par rapport à la version de l'API . Swagger est également plus simple à configurer . Cette stratégie est appelée gestion des versions via la négociation de contenu.
Dherik

3

Qu'en est-il simplement de l'utilisation de l'héritage pour modéliser la gestion des versions? C'est ce que j'utilise dans mon projet et cela ne nécessite aucune configuration de ressort spéciale et me donne exactement ce que je veux.

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

Cette configuration permet une faible duplication du code et la possibilité d'écraser des méthodes dans de nouvelles versions de l'API avec peu de travail. Cela évite également de compliquer votre code source avec une logique de changement de version. Si vous ne codez pas un point de terminaison dans une version, il récupérera la version précédente par défaut.

Comparé à ce que font les autres, cela semble beaucoup plus facile. Y a-t-il quelque chose qui me manque?


1
+1 pour partager le code. Cependant, l'héritage le couple étroitement. Au lieu. les contrôleurs (Test1 et Test2) devraient être juste un passage ... aucune implémentation logique. Tout la logique doit être dans la classe de service, someService. Dans ce cas, utilisez simplement une composition simple et n'héritez jamais de l'autre contrôleur
Dan Hunex

1
@ dan-hunex Il semble que Ceekay utilise l'héritage pour gérer les différentes versions de l'API. Si vous supprimez l'héritage, quelle est la solution? Et pourquoi le couple serré pose-t-il problème dans cet exemple? De mon point de vue, Test2 étend Test1 car c'est une amélioration de celui-ci (avec le même rôle et les mêmes responsabilités), n'est-ce pas?
jeremieca

2

J'ai déjà essayé de versionner mon API en utilisant le contrôle de version d' URI , comme:

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

Mais il y a des défis à relever lorsque vous essayez de faire en sorte que cela fonctionne: comment organiser votre code avec différentes versions? Comment gérer deux (ou plus) versions en même temps? Quel est l'impact lors de la suppression d'une version?

La meilleure alternative que j'ai trouvée n'était pas la version de l'API entière, mais le contrôle de la version sur chaque point de terminaison . Ce modèle est appelé Contrôle de version à l'aide de l'en-tête Accept ou Contrôle de version via la négociation de contenu :

Cette approche nous permet de versionner une représentation de ressource unique au lieu de versionner l'ensemble de l'API, ce qui nous donne un contrôle plus granulaire sur le contrôle des versions. Cela crée également une empreinte plus petite dans la base de code car nous n'avons pas à bifurquer l'application entière lors de la création d'une nouvelle version. Un autre avantage de cette approche est qu'elle ne nécessite pas d'implémenter les règles de routage URI introduites par le contrôle de version via le chemin URI.

Mise en œuvre au printemps

Tout d'abord, vous créez un contrôleur avec un attribut produit de base, qui s'appliquera par défaut pour chaque point de terminaison à l'intérieur de la classe.

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

}

Après cela, créez un scénario possible dans lequel vous disposez de deux versions d'un point de terminaison pour créer une commande:

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

Terminé! Appelez simplement chaque point de terminaison en utilisant la version d'en- tête Http souhaitée :

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

Ou, pour appeler la version deux:

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

À propos de vos inquiétudes:

Et comme toutes les méthodes d'une API ne changent pas dans la même version, je ne veux pas accéder à chacun de mes contrôleurs et changer quoi que ce soit pour un gestionnaire qui n'a pas changé entre les versions

Comme expliqué, cette stratégie maintient chaque contrôleur et point de terminaison avec sa version réelle. Vous ne modifiez que le point de terminaison qui a des modifications et a besoin d'une nouvelle version.

Et le Swagger?

Configurer le Swagger avec différentes versions est également très facile en utilisant cette stratégie. Voir cette réponse pour plus de détails.


1

En produit, vous pouvez avoir une négation. Donc pour method1 disproduces="!...1.7" et dans méthode2 ont le positif.

Le produit est aussi un tableau donc vous pour la méthode1 vous pouvez dire produces={"...1.6","!...1.7","...1.8"}etc (tout accepter sauf 1.7)

Bien sûr, pas aussi idéal que les plages que vous avez à l'esprit, mais je pense que plus facile à maintenir que d'autres éléments personnalisés si c'est quelque chose de rare dans votre système. Bonne chance!


Merci codesalsa, j'essaie de trouver un moyen facile à entretenir et qui ne nécessite pas que certains mettent à jour chaque point de terminaison à chaque fois que nous devons modifier la version.
Augusto

0

Vous pouvez utiliser AOP, autour de l'interception

Envisagez d'avoir un mappage de demande qui reçoit tous les /**/public_api/*et dans cette méthode ne rien faire;

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

Après

@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


}

La seule contrainte est que tout doit être dans le même contrôleur.

Pour la configuration d'AOP, consultez http://www.mkyong.com/spring/spring-aop-examples-advice/


Merci hevi, je cherche une façon plus conviviale de faire cela, car Spring sélectionne déjà la méthode à appeler sans utiliser AOP. Je suis d'avis que l'AOP ajoute un nouveau niveau de complexité du code que je voudrais éviter.
Augusto

@Augusto, Spring a un excellent support AOP. Tu devrais l'essayer. :)
Konstantin Yovkov
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.