liksi logo

Génération de clients REST avec Swagger et Maven dans un contexte Code-First

Tristan Denmat - Publié le 15/06/2020
Génération de clients REST avec Swagger et Maven dans un contexte Code-First

Dans cet article, nous allons démystifier quelques idées fausses concernant l’implémentation des clients d’API REST :

  • Non, ce n’est pas parce que l’on fait une API Rest qu’on ne doit pas spécifier les formats de données
  • Non, ce n’est pas parce que l’on appelle une API Rest que l’on doit écrire les messages JSON à  la main
  • Non, ce n’est pas parce que la description Swagger de l’API a été faite en mode Code-first qu’on ne peut pas l’utiliser pour générer du code

Microservices et api rest

Si vous avez suivi la tendance de ces dernières années, vous avez probablement remplacé vos vieux monolithes de 100 000 lignes de code par des “micro-services”. Et vous en avez profité pour remplacer vos vieilles APIs SOAP par des APIs REST toutes neuves. Vous avez donc sous la main tout un tas de petits projets Java qui essaient de se parler. Si vous travaillez à plusieurs sur ces projets, vous avez forcément été confronté à la problématique de communication entre les équipes de dev au sujet des APIs. C’est là que la première idée fausse intervient : même si votre API n’est plus aussi stricte qu’en SOAP, ça reste intéressant d’utiliser un formalisme pour la décrire :

  • Vous aurez un document de travail pour échanger entre équipes de dev, avec des clients ou avec votre métier
  • Vous pourrez facilement comparer deux versions de votre API
  • Vous pourrez bénéficier d’outils qui exploitent cette description (génération de documentation, génération d’un portail de test, …)

Open-api et Swagger

Plusieurs formalismes de description d’API ont été proposés depuis 2013, les principaux étant RAML, API Blueprint et Swagger. Ce dernier a donné lieu à la création du projet open-source Open API Specification (https://www.openapis.org/) et s’est finalement imposé comme standard pour la définition d’API REST. Les spécifications Open-API peuvent être utilisées de deux manière : soit en mode Contract-First, soit en mode Code-First.

Le but de l’article n’est pas de discuter des avantages et inconvénients de chaque méthode. D’une part cela entraîne souvent des débats sans fin, d’autre part l’historique des projets fait qu’on ne choisit pas toujours la méthode utilisée (et changer de méthode en cours de route est compliqué, notamment pour la raison 1/). En revanche de nombreux articles se sont intéressés à la question (voir par exemple Contract First vs Code First)

Contract-First

On appelle contract-first le fait d’écrire la spécification de l’API avant le code. Pour ça, des outils tels que Swagger Editor fournissent un éditeur agréable avec de la mise en forme automatique et de l’auto-complétion. À titre personnel, je considère que ce mode favorise le respect des bonnes pratiques de design d’API puisque l’on va façonner l’API sans avoir la tête dans le code. Du coup, on est amené à passer du temps sur ce genre de sites : API Stylebook au lieu de foncer tête baissée dans ce genre d’endroits : Stackoverflow Spring+Hateoas

Code-First

À l’inverse, l’approche code-first consiste à implémenter tout ou partie du service puis à annoter le code pour que le contrat d’interface OpenAPI puisse être généré automatiquement. L’intérêt principal de cette méthode est la rapidité (réelle ou supposée) : les développeurs pensent souvent que l’écriture de la spécification sera aussi longue que l’implémentation. Avec l’approche code-first, les deux sont faits simultanément.

Ecriture d’un client REST

En SOAP, écrire un client était très laborieux, d’une part à cause du protocole lui-même, d’autre part à cause des bibliothèques de génération de XML qui n’étaient pas très agréable à utiliser. À l’inverse, pour faire un appel REST il vous suffit de prendre vos bibliothèques HTTP et JSON (NB : on peut aussi faire du REST avec du XML) et en quelques minutes vous êtes capables d’envoyer les premières requêtes. Écrire un client “à la main” est donc tentant et c’est ce qui est fait dans de nombreux projets, on n’a pas tué WSDL pour rien après tout.

Malheureusement, même si cette approche permet probablement une première livraison rapide, on se retrouve rapidement à perdre du temps sur les livraisons suivantes : il faut répercuter des modifications de beans dans plusieurs projets, corriger des clés mal orthographiées, …

Swagger Codegen à la rescousse 

Swagger codegen permet de générer des clients REST (pas uniquement Java d’ailleurs) à partir de la description de l’API au format Open API. Comme on va le voir, cela vous permet de générer automatiquement du code client simplement grâce une configuration maven. Surtout, ce code client sera régénéré à chaque build et sera mis à jour si l’API évolue.

Le contrat d’API en tant que dépendance MAVEN

De la même manière que vous utilisez maven pour dire que votre projet dépend de la version 1.2.48 de votre bibliothèque préférée, vous allez pouvoir dire que votre projet dépend de la version 1.1.12 de l’API que vous appelez.

Cela permet :

  1. d’expliciter cette dépendance
  2. de la déclarer de la même manière que n’importe quelle autre dépendance.

En mode contract-first, l’utilisation de maven pour versionner l’API semble assez naturel mais ce n’est pas un pré-requis : l’exemple ci-dessous montre comment publier le contrat en tant que dépendance maven, même dans le cas de l’utilisation de Swagger en mode code-first.

Exemple

Dans la suite, on prend pour exemple une application de blog composée de deux micro-services exposant des APIs REST

  • Blog-service : Un service permettant de créer, modifier et supprimer des articles
  • iam : Un service de gestion de comptes utilisateur

Le micro-service de blog s’appuie sur la gestion de comptes et embarque donc un client REST pour l’API iam. Le code complet de cet exemple est visible sur le repo https://github.com/liksi/sample-blog-service

La suite de l’article détaille les étapes suivantes :

  1. Utilisation de SpringBoot et springfox pour exposer Swagger UI sur les     micro-services (mode code-first
  2. Génération du contrat d’interface
  3. Publication du contrat d’interface en tant qu’artefact maven
  4. Génération automatique du client iam dans blog-service
  5. Utilisation du code généré

Utilisation de Swagger UI dans Springboot

Les deux micro-services sont des projets standard Spring Boot, ils utilisent Spring MVC pour fournir la couche REST et Springfox pour l’exposition de Swagger UI.

Il existe de nombreux tutoriaux en ligne pour mettre en place une telle architecture, par exemple celui-ci : https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api

Si vous suivez toutes les étapes de ce tutoriel vous obtiendrez :

  • Une belle IHM publiée à l’url /swagger-ui.html vous permettant de tester votre API
  • Le fichier de description de l’API exposé sur /v2/api-docs. NB : ce fichier est d’ailleurs utilisé par l’IHM swagger UI pour générer les différents widgets.

Génération du contrat d’interface

L’idée va maintenant être de configurer maven pour que le build du projet “iam” produise le fichier de description de l’API. Pour cela, deux possibilités : l’utilisation d’un plugin maven dédié et la récupération du fichier via un appel au endpoint /v2/api-docs.

Utilisation du plugin swagger-maven-plugin

Le plugin swagger-maven-plugin est développé principalement par Yukai Kong depuis 2013 et une version “estampillée Open API” a été créée en 2017. Ces deux versions semblent très similaires, la version “Open-API” déclare d’ailleurs officiellement être inspirée du plugin initial.

Nous avons testé les deux versions et obtenu des résultats équivalents sur notre exemple. Cependant, nous ne détaillerons pas plus cette approche qui nous semble moins pertinent que la deuxième piste. En effet :

  • Toutes les annotations Spring MVC ne sont pas supportées
    Par exemple, @PostMapping doit être remplacé par @RequestMapping(method = RequestMethod.POST)
  • Les définitions des paramètres de sécurité sont dupliquées dans la configuration Springfox et le plugin maven. De même pour les méta-données de l’API.
  • Le fichier de description généré n’est pas identique au fichier utilisé par Springfox pour générer la page Swagger-UI. Ainsi, il se peut que l’on ait des divergences entre les tests effectués via Swagger-UI et les appels effectués via des clients dérivés du fichier produit par le plugin maven.

Utilisation du fichier de description produit par Springfox

Dans l’approche Code-First, Swagger-UI a un rôle prépondérant puisque c’est souvent de là que sont effectués les premiers tests et démos de l’API. Il est donc logique de considérer Swagger-UI comme étant la référence du comportement de l’API. Ainsi, le plus simple est de récupérer directement de fichier produit par Springfox. Pour cela, nous ajoutons le test suivant dans les classes de test du projet :

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IamApplicationTests {

   @LocalServerPort
   int randomServerPort;

   @Test
   public void getSwaggerJsonFile() throws IOException {

      String swagger = new RestTemplate().getForObject("http://localhost:" + randomServerPort + "/v2/api-docs", String.class);

      Path path = Paths.get(System.getProperty("user.dir"), "target", "iam.json");

      FileCopyUtils.copy(swagger.getBytes(StandardCharsets.UTF_8),  path.toFile());
   }
}

Ce test démarre simplement l’application SpringBoot sur un port libre, puis une requête est effectuée sur le path /v2/api-docs pour récupérer le fichier de description d’API utilisé par Swagger-UI. Ce fichier est ensuite copié dans le répertoire target du projet.

Publication du contrat d’interface en tant qu’artefact maven

Pour que le contrat d’interface soit utilisable par d’autres projets, nous allons le publier en tant qu’artefact maven. Pour cela, il y a deux cas :

  • Soit l’artefact principal du projet est déjà déployé par maven. Dans ce cas, nous ajouterons le contrat d’interface en tant qu’artefact secondaire
  • Soit, comme c’est de plus en plus souvent le cas avec docker, le fichier .jar généré par maven n’est pas déployé sur un dépôt maven. En effet, comme une image docker contenant le .jar est conservée sur un registre Docker, le stockage dans le dépôt maven est inutile. Dans ce cas, nous configurerons les plugins maven-install-plugin et maven-deploy-plugin pour ne déployer que le contrat d’interface.

Cas 1 – déploiement d’un artefact secondaire

Le plugin swagger-maven-plugin ajoute par défaut le contrat d’interface comme artefact secondaire. En revanche, le fichier récupéré via le test “getSwaggerJsonFile” doit être explicitement ajouté aux artefacts. Pour cela, il suffit d’utiliser le plugin build-helper-maven-plugin :

<plugin>
   <groupId>org.codehaus.mojo</groupId>
   <artifactId>build-helper-maven-plugin</artifactId>
   <version>3.0.0</version>
   <executions>
      <execution>
         <id>attach-artifacts</id>
         <phase>package</phase>
         <goals>
         <goal>attach-artifact</goal>
         </goals>
         <configuration>
            <artifacts>
               <artifact>
                  <file>${project.build.directory}/iam.json</file>
                  <type>json</type>
                  <classifier>swagger</classifier>
               </artifact>
            </artifacts>
         </configuration>
      </execution>
   </executions>
</plugin>

Dans ce cas, l’utilisation de mvn install ou mvn deploy entraînera le déploiement de l’artefact principal (de type jar) et de l’artefact de type json et de classifier “swagger”, que ce soit dans le répertoire .m2 local ou dans les dépôts maven.

Cas 2 – déploiement du contrat d’interface uniquement

Avec l’avènement de docker, il arrive souvent que l’artefact maven ne soit pas archivé puisque cela serait redondant avec l’image stockée sur le registre docker. Ce cas est plus compliqué puisqu’on ne vas pas pouvoir utiliser les goals install et deploy des plugins maven standards. En effet, ces goals entraîneraient le déploiement de l’artefact principal.

Installation en local

Pour l’installation en local, nous ajoutons les propriétés suivantes :

<properties>
  <maven.install.skip>true</maven.install.skip>
  <file>${project.build.directory}/iam.json</file>
</properties>

Ainsi que le plugin :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-install-plugin</artifactId>
    <executions>
        <execution>
            <id>skip-main</id>
            <phase>install</phase>
            <goals>
                <goal>install-file</goal>
            </goals>
            <configuration>
                <pomFile>./pom.xml</pomFile>
                <packaging>json</packaging>
                <classifier>swagger</classifier>
            </configuration>
        </execution>
    </executions>
</plugin>

Cela a pour effet de :

  • Ne pas exécuter le goal install:install configuré par défaut sur la phase “install” de maven
  • À l’inverse, exécuter le goal install:install-file pendant la phase “install”, en déployant :
    • Le fichier target.iam.json (cf propriété “file”)
    • Avec les informations de groupId:artefactId:version définies dans le fichier pom.xml (le fichier courant)
    • En spécifiant le type json et le classifier swagger

Déploiement sur les dépôts Maven

Pour le déploiement, le même mécanisme est utilisé pour configurer le plugin maven-deploy. Vous trouverez le détail de la configuration dans le pom file du projet.

Génération automatique d’un client de l’API

Jusqu’ici, nous avons utilisé un test unitaire et maven pour déployer le fichier json de description de l’API en tant qu’artefact. Dans cette section, nous allons utiliser ce fichier pour générer le client de l’API “iam” au sein du module “blog-service”

Pour cela, la première chose va être de récupérer ce fichier json et de l’ajouter aux sources du projet “blog-service”. Cela se fait simplement avec le plugin maven-dependency-plugin :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>${plugin.maven.dependency.version}</version>
    <executions>
        <execution>
        <id>copy</id>
        <phase>generate-sources</phase>
        <goals>
            <goal>copy</goal>
        </goals>
        <configuration>
            <artifactItems>
                <artifactItem>
                    <groupId>fr.liksi.blog</groupId>
                    <artifactId>iam</artifactId>
                    <version>${iam.version}</version>
                    <type>json</type>
                    <classifier>swagger</classifier>
                    <overWrite>true</overWrite>
                    <outputDirectory>${project.build.directory}/generated-sources/swagger
                    </outputDirectory>
                    <destFileName>iam-api.json</destFileName>
                </artifactItem>
            </artifactItems>
        </configuration>
        </execution>
    </executions>
</plugin>

Dans cette configuration, le plugin va récupérer l’artefact publié à l’étape précédente puis le copier dans le répertoire target/generated-sources/swagger

NB : il peut être tentant de conserver une dépendance SNAPSHOT sur la version de l’API. Néanmoins, pour garantir un bon découplage des modules et des mises en production simples, je vous conseille fortement de figer la version de l’API utilisée par le client. Le service exposant l’API doit s’engager à maintenir une compatibilité descendante, le client ne changeant de version que lorsqu’il en a besoin.

Il ne reste désormais plus qu’à générer le code client Java grâce au plugin openapi-generator-maven-plugin :

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>3.3.4</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.build.directory}/generated-sources/swagger/iam-api.json</inputSpec>
                <generatorName>java</generatorName>
                <configOptions>
                    <library>resttemplate</library>
                    <dateLibrary>java8</dateLibrary>
                </configOptions>
                <apiPackage>fr.liksi.blog.connector.iam</apiPackage>
                <modelPackage>fr.liksi.blog.connector.iam.bean</modelPackage>
            </configuration>
        </execution>
    </executions>
</plugin>

Les différents paramètres de configuration du plugin sont listés ici. Les principaux sont :

  • inputSpec : L’emplacement du fichier de description de l’API

  • generatorName : le nom du générateur à utiliser ainsi que des options permettant de contrôler le code produit (voir ici la liste de tous les générateurs existants)   
    NB : Il est possible de créer son propre générateur pour cibler un langage non supporté, ou utiliser des librairies particulières

  • configOptions : paramétrage spécifique au générateur choisi. Dans notre exemple, on demande à utiliser resttemplate comme implémentation de la couche HTTP et les classes fournies par Java 8 pour la gestion des données de type “Date”. La liste     exhaustive des paramètres utilisables pour le générateur “java” est disponible ici.

  • apiPackage et modelPackage : noms de package utilisés pour la génération des objets “ressources” et des classes contenant les différentes méthodes de l’API.
    NB : si vous ne souhaitez pas que Springboot initialise automatiquement les objets produits par le générateur, il ne faut pas que la valeur choisie pour apiPackage soit un sous-package de la classe contenant l’annotation @SpringBootApplication. Cela est notament important si l’on souhaite maitriser par exemple la configuration de l’objet RestTemplate utilisé pour la connexion à l’API.

La configuration par défaut génère un projet complet incluant un fichier pom.xml, des tests unitaires, des fichiers gradle… Cela est intéressant si le but de la manœuvre est de produire un artefact maven complet pouvant être inclus dans d’autres projets.

  <generateModelDocumentation>false</generateModelDocumentation>
  <generateModelTests>false</generateModelTests>
  <generateApiTests>false</generateApiTests>
  <generateApiDocumentation>false</generateApiDocumentation>
  <generateSupportingFiles>true</generateSupportingFiles>
  <supportingFilesToGenerate>RFC3339DateFormat.java, ApiClient.java, ApiException.java, Configuration.java,JSON.java, Pair.java, StringUtil.java, TypeRef.java, ApiKeyAuth.java, Authentication.java, HttpBasicAuth.java,OAuth.java,OAuthFlow.java </supportingFilesToGenerate>

Dans notre cas on ne souhaite générer que les classes minimales pour les inclure dynamiquement dans les sources de notre projet au cours du build. Pour cela, il faut explicitement restreindre les classes à générer :

Génération des classes de l’API depuis IntelliJ

La commande mvn compile va générer automatiquement les classes de l’API pendant la phase “generate-sources”. En revanche, IntelliJ n’utilise pas cette phase lors de son build par défaut. Lors de la première importation du projet, ou pour prendre en compte un changement, il faut explicitement lancer cette phase en faisant “Clic droit sur le projet” > Maven > Generate Sources and Update Folder.

Utilisation des classes générées

Vous n’avez plus qu’à utiliser les classes générées au sein de votre code. Personnellement j’encapsule systématiquement les appels aux classes générées dans un connecteur chargé de découpler le code métier de l’API. Par exemple, l’appel à l’API “iam” est fait depuis la classe suivante :

@Service
public class IAMConnector {

   private AccountApi accountApi;

   @Autowired
   public IAMConnector(AccountApi accountApi) {
        this.accountApi = accountApi;
    }

   public Optional<Account> getAccount(final String email) {
     ApiAccount account = this.accountApi.getAccountUsingGET(email);
     Account result = null;
        if (account != null) {
            result = new Account();
            result.setEmail(account.getEmail());
            result.setFirstname(account.getFirstname());
            result.setLastname(account.getLastname());
        }
        return Optional.ofNullable(result);
    }
}

Au préalable, il faut avoir déclarer un Bean AccountApi dans une classe de Configuration Spring :

@Configuration
public class IAMClientConfig {

  @Value("${iam.url}")
    private String iamUrl;

  @Value("${iam.auth.user}")
    private String iamAuthUser;

  @Value("${iam.auth.password}")
    private String getIamAuthPwd;

    @Bean
    public AccountApi accountApi() {
        ApiClient apiClient = new ApiClient();
        apiClient.setBasePath(iamUrl);
        apiClient.setUsername(iamAuthUser);
        apiClient.setPassword(getIamAuthPwd);
        AccountApi accountApi = new AccountApi(apiClient);
        return accountApi;
    }

}

Conclusion

Voilà ! nous avons automatisé la génération de code client pour faire communiquer deux micro-services via une API REST. Ainsi, lorsqu’un changement aura lieu sur l’API iam, deux choix seront possibles :

  • Soit blog-service n’aura pas besoin de la nouvelle fonctionnalité et il ignorera la mise à jour. La rétro-compatibilité de l’API devra absolument être respectée.
    NB : un outil comme swagger-diff peut être utilisé pour vérifier qu’un changement d’API ne casse pas cette rétro-compatibilité.
  • Soit blog-service aura besoin de la fonctionnalité et il devra simplement :
    • Changer la version de la dépendance à iam
    • Modifier le code du connecteur pour intégrer une nouvelle méthode ou utiliser un nouveau paramètre, le tout sans duplication de code ou risque de typo dans les urls ou les noms des paramètres.

SOAP était sûrement une technologie trop complexe entraînant des incompatibilités entre les différentes implémentations et ne favorisant pas l’homogénéisation des APIs. Néanmoins, la génération de code à partir des fichiers WSDL n’avait pas que des mauvais côtés et je suis convaincu qu’il est dommage de se priver cette fonctionnalité lorsque l’on met en place des APIs REST.

J’espère que cet article vous aidera à remettre en place de la génération automatique de code client dans vos projets !

Derniers articles