112 votes

Comment gérer les API REST de gestion des versions avec le printemps?

J'ai été à la recherche de la façon de gérer une API REST versions à l'aide de Printemps 3.2.x, mais je n'ai pas trouver quelque chose qui est facile à entretenir. Je vais vous expliquer d'abord le problème que j'ai, et puis une solution... mais je me demande si je suis re-inventer la roue ici.

Je veux gérer la version basée sur 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 spring MVC transmettre à la méthode qui gère cette version. Et puisque toutes les méthodes de l'API de changement dans le même communiqué, je ne veux pas aller à chacun de mes contrôleurs et de changer quoi que ce soit pour un gestionnaire qui n'a pas changé entre les versions. Aussi, je ne veux pas la logique pour déterminer quelle version utilisée dans le contrôleur eux-mêmes (à l'aide du service des locators), comme le Printemps est déjà la découverte de la méthode à appeler.

Alors, pris d'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 répondre à ces questions de la manière suivante. Imaginez que le code est à l'intérieur d'un contrôleur, et qu'il y a un code qui est en mesure 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;
}

Ce n'est pas possible au printemps que les 2 méthodes ont le même RequestMapping d'annotation et de Printemps ne parvient pas à charger. L'idée est que l' VersionRange d'annotation est possible de définir une version ouverte ou fermée gamme. La première méthode est valable à partir de la version 1.0 à 1.6, tandis que la seconde pour la version 1.7 à partir de (y compris la dernière version 1.8). Je sais que cette approche des pauses si quelqu'un décide de passer de la version à 99,99, mais c'est quelque chose que je suis OK pour vivre avec.

Maintenant, depuis que ci-dessus n'est pas possible sans une sérieuse refonte de la façon dont des travaux du printemps, j'ai pensé à bricoler avec la façon dont les gestionnaires appariés à des demandes, en particulier pour écrire mes propres ProducesRequestCondition, et la version de la gamme là. 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 fermé ou ouvert, version plages définies dans le produit une partie de l'annotation. Je suis en train de travailler sur cette solution maintenant, avec le problème que j'avais encore à remplacer certains Spring MVC classes (RequestMappingInfoHandlerMapping, RequestMappingHandlerMapping et RequestMappingInfo), que je n'aime pas, parce que cela signifie un travail supplémentaire à chaque fois que je décide de mettre à niveau vers une version plus récente de printemps.

J'aimerais avoir des pensées... et surtout, une suggestion pour ce faire, dans un système plus simple, plus facile à maintenir façon.


Modifier

L'ajout d'une prime. Pour obtenir la prime, veuillez répondre à la question ci-dessus, sans suggérer d'avoir cette logique dans le contrôleur eux-mêmes. Le printemps a déjà beaucoup de logique pour sélectionner le contrôleur de la méthode à appeler, et je veux greffer sur que.

61voto

xwoker Points 1523

Indépendamment de savoir si le contrôle de version peut être évité en faisant compatible changements (ce qui n'est pas toujours possible quand vous êtes lié par certaines sociétés de lignes directrices ou de votre API clients sont mis en œuvre dans un buggy et qu'ils allaient se briser, même s'ils ne devraient pas) l'abstraction de l'exigence est intéressante:

Comment puis-je faire une demande personnalisée de cartographie qui ne arbitraire des évaluations de valeurs d'en-tête de la demande sans faire l'évaluation dans le corps de la méthode?

Comme décrit dans cette SORTE de réponse vous avez réellement peuvent avoir le même @RequestMapping et utiliser un autre annotation à se différencier au cours de l'acheminement qui se passe au cours de l'exécution. Pour ce faire, vous devrez:

  1. Créer une nouvelle annotation VersionRange.
  2. Mettre en œuvre un RequestCondition<VersionRange>. Puisque vous avez quelque chose comme un meilleur algorithme de correspondance, vous aurez pour vérifier si les méthodes annotées avec d'autres VersionRange valeurs fournir une meilleure adéquation de la demande en cours.
  3. Mettre en œuvre un VersionRangeRequestMappingHandlerMapping basée sur l'annotation et à la demande de l'état (comme décrit dans le message lié).
  4. Configurer le printemps pour évaluer votre VersionRangeRequestMappingHandlermapping avant d'utiliser la valeur par défaut RequestMappingHandlerMapping (par exemple en fixant sa commande à 0).

Ce ne serait pas exiger de toute hacky replackements de Printemps composants, mais utilise le Printemps de configuration et les mécanismes d'extension de sot cela devrait fonctionner même si vous mettez à jour votre Printemps version (tant que la nouvelle version prend en charge ces mécanismes.

51voto

Benjamin M Points 2206

Je viens de créer une solution personnalisée. Je suis à l'aide de l' @ApiVersion d'annotation en combinaison avec d' @RequestMapping d'annotation à l'intérieur d' @Controller les classes.

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

}

Mise en œuvre:

ApiVersion.java annotation:

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

ApiVersionRequestMappingHandlerMapping.java (c'est surtout du copier-coller d' 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);

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

}

L'Injection dans WebMvcConfigurationSupport:

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

17voto

elusive-code Points 454

Je voudrais encore vous recommandons d'utiliser des URL pour les versions, car dans les URLs @RequestMapping prend en charge les modèles et chemin de paramètres, dont le format peut être spécifiée avec des regexp.

Et pour gérer les mises à niveau du client (que vous avez mentionné dans le commentaire), vous pouvez utiliser des alias comme "plus tard". Ou avez sans version de version de l'api qui utilise la dernière version (ouais).

Aussi à l'aide de chemin paramètres que vous pouvez implémenter une version complexe de la manipulation de la logique, et si vous voulez déjà avoir des plages, vous avez très probablement vouloir quelque chose de plus assez vite.

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

Basé sur la dernière approche, vous pouvez effectivement mettre en place quelque chose comme ce que vous voulez.

Par exemple, vous pouvez avoir un contrôleur qui ne contient que de la méthode de la transperce avec la version de la manipulation.

Dans cette manipulation vous regardez (à l'aide de réflexion/AOP/génération de code bibliothèques) dans certains ressorts de service/composante ou dans la même classe pour la méthode avec le même nom/signature et nécessaires @VersionRange et à l'appeler le passage de tous les paramètres.

8voto

Willie Wheeler Points 8632

L' @RequestMapping d'annotation prend en charge un headers élément qui vous permet d'affiner la requête correspondante. En particulier, vous pouvez utiliser l' Accept - tê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 n'est pas directement gérer les plages, mais l'élément prend en charge le caractère générique * ainsi que !=. Si au moins vous pouviez sortir avec l'aide d'un joker pour les cas où toutes les versions de soutenir l'effet en question, ou même de toutes les versions mineures d'une version majeure (par exemple, 1.*).

Je ne pense pas en fait, j'ai utilisé cet élément avant (si je l'ai je ne m'en souviens pas), je vais donc juste au large de la documentation à

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

1voto

codesalsa Points 242

Dans produit vous pouvez avoir de la négation. Donc, pour method1 dire produces="!...1.7" et dans method2 ont le positif.

Le produit est également un tableau de sorte que vous pour method1 vous pouvez dire produces={"...1.6","!...1.7","...1.8"} etc (accepter tous sauf 1.7)

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X