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 RequestMapping
annotation et Spring ne se charge pas. L'idée est que l' VersionRange
annotation 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
, RequestMappingHandlerMapping
et 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
la source
produces={"application/json-1.0", "application/json-1.1"}
Réponses:
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
@RequestMapping
et 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:VersionRange
.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'autresVersionRange
valeurs fournissent une meilleure correspondance pour la requête actuelle.VersionRangeRequestMappingHandlerMapping
basé sur l'annotation et la condition de demande (comme décrit dans l'article Comment implémenter les propriétés personnalisées @RequestMapping ).VersionRangeRequestMappingHandlerMapping
avant d'utiliser la valeurRequestMappingHandlerMapping
par 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).
la source
mvc:annotation-driven
. Espérons que Spring fournira une version demvc:annotation-driven
dans laquelle on pourra définir des conditions personnalisées.Je viens de créer une solution personnalisée. J'utilise l'
@ApiVersion
annotation en combinaison avec l'@RequestMapping
annotation à l'intérieur des@Controller
classes.Exemple:
La mise en oeuvre:
Annotation ApiVersion.java :
ApiVersionRequestMappingHandlerMapping.java (il s'agit principalement de copier-coller à partir de
RequestMappingHandlerMapping
):Injection dans WebMvcConfigurationSupport:
la source
/v1/aResource
et/v2/aResource
ressemblent à 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.RequestMappingHandlerMapping
dans votreWebMvcConfiguration
, vous devez remplacercreateRequestMappingHandlerMapping
au lieu derequestMappingHandlerMapping
! 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)WebMvcConfigurationSupport
mais étendreDelegatingWebMvcConfiguration
. Cela a fonctionné pour moi (voir stackoverflow.com/questions/22267191/… )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:
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.
la source
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:
En- tête Content-Type , dans lequel le client définit la version dans l'en- tête Accept :
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:
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" .
Afin de l'implémenter au printemps, nous devons remplacer le comportement par défaut de RequestMappingHandlerMapping :
}
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:
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/company
et nous demandons au client de passer à la v2 à la place sur la v1.Ainsi, les nouvelles demandes du client sont:
au lieu de:
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:
GIT
Implémentation de la solution sur: https://github.com/mspapant/restVersioningExample/
la source
L'
@RequestMapping
annotation prend en charge unheaders
élément qui vous permet de restreindre les demandes correspondantes. En particulier, vous pouvez utiliser l'en-Accept
tête ici.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
la source
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 printempsMediaType
œuvresQu'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.
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?
la source
J'ai déjà essayé de versionner mon API en utilisant le contrôle de version d' URI , comme:
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 :
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.
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:
Terminé! Appelez simplement chaque point de terminaison en utilisant la version d'en- tête Http souhaitée :
Ou, pour appeler la version deux:
À propos de vos inquiétudes:
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.
la source
En produit, vous pouvez avoir une négation. Donc pour method1 dis
produces="!...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!
la source
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;Après
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/
la source