Basculer son code vers JAVA 8 sur un projet à grande échelle

J’accompagne Voyages SNCF (VSCT) depuis plus de deux ans en tant qu’Architecte de suivi de production chez PALO IT. J’interviens notamment sur les problématiques lilées à la mise en production de nouvelles versions des applications. A travers cet article, je vais vous faire part de mon retour d’expérience suite à ma participation à un hackathon intitulé « Faire basculer VSCT vers Java 8 ».

Introduction

Alors que la version 5 de Java avait permis l’introduction de classes/types génériques et d’une nouvelle API, cette évolution avait en réalité eu peu d’impact sur la manière de développer, et le temps d’adaptation pour une équipe était négligeable. Java 8 n’est pas une simple nouveauté : c’est une véritable révolution ! En effet, c’est une logique toute entière de conception et de développement qui change, en introduisant principalement la logique de programmation fonctionnelle (expression Lambda) comme les « functional interfaces » et les nouvelles API de collection « Stream ».

Cette nouveauté remet en question toute une pratique de développement (de plus de 10 ans pour moi !) et si le passage à Java 8 peut s’effectuer sans difficultés pour certains (en particulier pour les jeunes développeurs fraîchement diplômés), il pourrait s’avérer plus complexe pour ceux qui n’auraient pas encore eu l’occasion de découvrir les principaux langages fonctionnels tes que « Scala » et/ou « Clojure ».

Contexte et enjeux du hackaton

Dans une démarche d’expérimentation, VSCT organise chaque année un hackathon auquel tous les collaborateurs peuvent participer. Une pré-sélection des sujets de tous types est organisée par un jury, qui identifie ensuite les équipes participant à cet événement. J’ai alors proposé l’étude de la migration du code existant vers Java 8, en appliquant la nouvelle syntaxe des expressions Lambda sur certaines classes cibles.

Le site Voyages SNCF est une application critique de haute disponibilité, devant offrir une qualité et un délai de service irréprochables pour ses utilisateurs. Dans ce contexte, les améliorations vendues par Java 8 en termes de qualité de code et de performance étaient intéressantes. Cependant, le lancement d’un plan de migration sans stratégie préalable aurait pu s’avérer risqué, les équipes travaillant en mode Agile et les demandes étant gérées par ordre de priorité. Ajouter à cela un tel chantier aurait forcément nécessité un nombre d’homme/jour supplémentaire, sans compter le délai de montée en compétences, ainsi que le risque de répercussions négatives sur les performances de l’application si le code design de la nouvelle implémentation n’est pas soigné.

Sans oublier qu’il ne s’agit pas d’un développement « from scratch » : on parle bien de code existant qu’il faudrait reprendre en Java 8. Si ce dernier est complexe et/ou mal développé, le « Refactoring » en serait d’autant plus délicat. Mises à part les contraintes de délai, on risquerait de se retrouver avec des performances dégradées malgré les promesses vendues par Java 8 et dans ce cas de figure, peu d’espoir que le sujet ait encore un grand intérêt.

Conscients des possibilités offertes par ce nouveau style de développement et prêts à relever le challenge, les Développeurs des différentes équipes étaient déjà très motivés à l’idée de basculer vers Java 8. Ce ne sont pas les compétences qui manquent chez VSCT mais ce projet de migration ne remportait pas l’adhésion du Management, pour les raisons citées précédemment. Le hackathon représentait cependant une belle opportunité pour expérimenter cette idée et obtenir un premier résultat qui apporterait une meilleure visibilité quant à la priorisation de ce chantier de migration.

Dans le cadre de ce hackaton, nous avons opté pour le déroulement des étapes suivantes :

  • Constitution de l’équipe;
  • Préparation de l’environnement de travail;
  • Transformation du code en Java 8;
  • Exécution des tests unitaires ISO sans Java 8;
  • Tests de performance.

Etape 1 : consitution de l’équipe par niveaux

Le hackathon permettrait en premier lieu la montée en compétences des équipes internes. Nous avons donc formé une équipe hétérogène, en nous basant sur nos différents niveaux de maîtrise de Java 8 :

  • Moi-même au niveau avancé : dans le cadre de mon expérience personnelle, je rassemblais des connaissances depuis 5 mois grâce au développement de mini-exemples, la lecture d’ouvrages et d’articles sur les bonnes pratiques de développement, la participation aux événements Java 8, etc;
  • Hichem au niveau moyen : il avait commencé à s’approprier les notions de base des nouveautés quelques jours avant;
  • Dhia au niveau débutant : il n’avait aucune connaissance Java 8 préalable.

NB : Pour rappel, les membres de l’équipe sont tous des Architectes de suivi de production et des Experts Java.

Nous avons ensuite identifié des classes significatives, qui sont souvent appelées lors de l’exécution, et sur lesquelles nous pouvions appliquer les transformations issues des nouvelles API « Stream » ou « Collectors ». Nous avons fini par identifier 8 classes simples, 8 classes moyennes et 8 classes complexes réparties sur 3 projets distincts. Leur attribution s’est faite de manière aléatoire, ce qui nous a permis de dresser 3 tableaux que nous analyserons plus bas.

Etape 2 : migration et exécution de tests unitaires

Une fois les rôles définis et les classes identifiées, nous avons débuté la phase de migration :

  • Préparation des environnements de travail : mise en place des projets en local + compilation Java 8 + configuration de l’IDE + création des projets sur Git [durée : 2H30];
  • Présentation de Java 8 axée autour des grandes nouveautés et des best practices à prendre en compte [durée : 1H30];
  • Début du développement : j’ai dédié 70% de mon temps au développement et 30% au support des autres membres de l’équipe. Chaque classe redéveloppée n’est considérée finie que si le Code Design est respecté (éviter les « Foreach » au maximum) et que les résultats des tests unitaires sont ISO code sans les expressions Lambda [durée : 1 jour];
  • Pour assurer la qualité du code développé, nous avons utilisé les plugins suivants sur « IntelliJ» : « QAPlug», « FindBugs » et « CheckStyle », qui sont compatibles avec les nouveautés Java8. Ci-dessous quelques résultats d’analyses appliquées sur du code réécrit en Java 8 affichant des recommandations d’amélioration sur l’utilisation de l’API « Stream » :

Code1

 

Code2

Nous avons souhaité utiliser « SonarQube », mais pour des raisons de Timing, nous n’avions pas pu le mettre en place vu le coût d’installation et de configuration que ça pouvait engendrer. Le résultat de notre avancement est récapitulé dans les 3 tableaux ci-dessous. La ligne Equipe récapitule le pourcentage du nombre de méthodes migrées par rapport à la cible et le temps passé en moyenne par personne et par méthode transformée (tests unitaires inclus) :

Tab 1

Tableau 1 : « Statut avancement refactoring Java 8 des classes simples »

 

Tab 2

Tableau 2 : « Statut avancement refactoring Java 8 des classes moyennes »

 

Tab 3

Tableau 3 : « Statut avancement refactoring Java 8 des classes complexes »

 

Ci-dessous quelques exemples de bouts de code changés en Java 8 en utilisant les expressions Lambda en notant à chaque fois le temps passé et des remarques sur les difficultés trouvées :

Exemple 1
public List<FrequentTravellerCardTypeEnum> getFrequentTravellerAdvantage() {   

    final List<ProgrammeDeFidelite> fids = getProgrammesDeFidelite();

 /* Ancien Code

    final List<FrequentTravellerCardTypeEnum> result = new                ArrayList<FrequentTravellerCardTypeEnum>(fids.size());

    for (final ProgrammeDeFidelite f : fids) {

        final Integer id = Integer.valueOf(f.getCode());

        if (FrequentTravellerCardTypeEnum.getById(id) != null) {

            result.add(FrequentTravellerCardTypeEnum.getById(id));

        } else {

            logCarteInconnu(f)

        }

    }

    return result;

    */

    // nouveau code Java8

    // 20 mn

     return fids.stream()

                              .mapToInt(fid -> Integer.valueOf(fid.getCode()))

                              .mapToObj(FrequentTravellerCardTypeEnum::getById)

                              .peek(e-> {

                   if (e.getCode()) logCarteInconnu(e);

                })

                              .filter(e -> e != null);

}

 

Exemple 2
/* Ancien code

 PaymentCard carteARendrePrincipale = null;

 for (final PaymentCard carte : lesCartesDePaiement) {

     if (!carte.isExpired()

             && !carte.isDefault()

             && (carteARendrePrincipale == null || carteARendrePrincipale.getCreationDate().before(

             carte.getCreationDate()))) {

         carteARendrePrincipale = carte;

     } */

 // java8 20 mn

 PaymentCard carteARendrePrincipale = lesCartesDePaiement.stream()

         .filter(carte-> !carte.isExpired() && !carte.isDefault())

         .min(Comparator.comparing(carte->carte.getCreationDate().getTime()))

         .orElse(null);

 

Exemple 3
/*public ReturnEnumeratedType execute(final IContext vscContext, final FinalizationServiceValueObject vo,final boolean moduleDisabled) {

        final CommandContext context = createContext(vscContext, vo, moduleDisabled);

        for (int i = 0; i < commands.size(); i++) {             final boolean result = commands.get(i).execute(context);             if (!result) {                 commands.get(i).cleanup(context);                 for (int j = i - 1; j > -1; j--) {

                    commands.get(j).rollback(context);

                }

                return context.getStatus();

            }

        }

        return context.getStatus();

    }*/

/*-----------------------------------------------------------------------Java8----------------------------------------------------------          Nous avons passé 1h30 sur cette méthode

Difficulté de transformer les boucles imbriquées avec les expressions Lambda sans passer par Forereach

----------------------------------------------------------------------------------------------------------------------------------------*/

public ReturnEnumeratedType executeJava8(final IContext vscContext, final FinalizationServiceValueObject vo, final boolean moduleDisabled) {

        final CommandContext context = createContext(vscContext, vo, moduleDisabled);

        List<FinalizationCommand> finalizationCommands = new ArrayList<>();

        commands.stream()

                .filter(com->!com.execute(context))

                .forEach(e->{

                    finalizationCommands.add(e);

                    e.cleanup(context);

                    finalizationCommands.

                            stream().

                            forEach(ep->ep.rollback(context));

                });

        return context.getStatus();

    }

Etape 3 : tests de performance

  • Notre collègue « Hicham» nous a proposé d’utiliser « Contiperf » (http://databene.org/contiperf), un utilitaire qui nous a permis de dérouler des tests de performance sur certains tests unitaires Java 8 vs sans Java 8. Cet outil permet de générer un rapport contenant le temps d’exécution moyen, en percentile et le temps de latence maximum de chaque test.

NB : Nous avons configuré un comportement du « StressTest » représentatif de la charge de production, ce qui consiste à injecter un grand nombre d’itérations en parallèle et non de gros volumes de donnés.

  • « Profiling » du code avec « Jprofiler » afin de mesurer la quantité mémoire consommée pour chaque test déroulé. Les illustrations suivantes présentent un rapport du résultat d’un échantillon de « StressTest » tel que le génère « Contiperf » :

 

Contiperf1

1. Rapport du résultat : ContiPerfReport

Comme vous pouvez le constater, les performances du code Java8 sont deux fois meilleures sur ce « StressTest ».

Contiperf2

2. Rapport du résultat : ContiPerfReport

 

Contrairement à la première illustration, sur ce test le code Java8 est deux fois moins performant. Conclusion : le code est à revoir !

Résultats et interprétation

  • Nous avons réussi à migrer la totalité des classes simples avec une moyenne de 29 minutes par service, y compris les tests unitaires qui vont avec.

> C’est un résultat encourageant pour une équipe composée des niveaux mixtes. Le partage de connaissances et l’accompagnement des autres membres semblent avoir été efficaces.

  • Seules les classes moyennes et complexes ont été modifiées. Par rapport aux classes simples, le temps moyen de transformation est estimé au double (1H00 / méthode) pour les classes moyennes et, au minimum, au triple (<1h30 / méthode) pour les classes complexes.

> Une connaissance plus approfondie sur les pratiques avancées des nouvelles APIs Java 8 est nécessaire. Si le graphique de dépendance des classes est de plus en plus complexe, l’impact du « Refactoring» peut facilement dépasser le changement du code lui-même.

  • Au niveau des performances, certains tests ont donné de bons résultats en termes de temps d’exécution et de consommation mémoire, quand d’autres sont moins bons (surtout pour le cas de classes complexes). Globalement, les performances sont équivalentes. Notez que nous n’avons pas testé les « ParallelStream », ceux-ci étant peu adaptés à nos volumes de production.

> Les conclusions restent à confirmer dans le cadre d’un vrai test de charge. Il est aussi probable que certains « Refactoring » et transformations devraient être revus compte tenu de leurs résultats insatisfaisants, et ce indépendamment de la performance Java 8.

Les tableaux suivants récapitulent le résultat comparatif des temps d’exécution et de consommation mémoire sur un cas de « StressTest » d’une classe simple, moyenne et complexe : résultat concluant pour les classes simples, mais le code est à revoir pour des cas de classes complexes.

Tab 1

Tab 2

Tab 3

Conclusion

L’exercice de « Refactoring » que nous avons appliqué vient confirmer l’avantage d’utiliser les expressions Lambda de Java 8 : un code plus simple, avec moins de typages inutiles, plus maintenable et moins verbeux. Cet exercice de « Refactoring » sur une « Codebase » réelle est très intéressante : il permet une courbe d’apprentissage extrêmement rapide pour un investissement somme toute relatif. Pas de dégradation des performances sur les transformations simples grâce à une pratique correcte des expressions Lambda. Cela n’a pas été le cas sur les classes complexes, montrant un risque de dégradation lié d’une part à des « Refactoring » importants sur du code complexe et d’autre part à une mauvaise utilisation des expressions Lambda.

Par rapport aux résultats attendus de ce hackaton, nous n’avons pas pu transformer la totalité des classes et les performances n’étaient pas au rendez-vous, surtout sur les classes moyennes et complexes. Nous n’avons donc pas pu diffuser un message rassurant tel que nous l’avions souhaité au début de cet article.

Développer avec les expressions « Lambda » et les nouvelles API Java 8 ne se fait pas du jour au lendemain, surtout quand il s’agit d’une réécriture de code dans le cadre d’un grand projet à haute disponibilité. Une bonne approche pour poursuivre cette migration serait donc :

  • D’adopter une stratégie de migration progressive en ciblant un projet pilote;
  • De faire émerger de ce projet un pôle de compétences Java8/Lambda;
  • D’utiliser ce pôle pour permettre un transfert de connaissances plus rapide et un retour d’expérience plus réaliste aux autres équipes;
  • D’assurer la montée en compétences générale en multipliant les workshops et les évènements autour du sujet;
  • D’inciter à la bonne pratique d’utilisation des outils de code qualité existants et compatibles Java 8 tels que « PMD», « FindBugs », « CheckStyle » et « SonarQube ».

Ensuite, comme souvent pour ce type de migration, il sera plus efficace de les inclure dans des projets de refonte déjà planifiés dans les roadmaps projets, voire des projets métiers, plutôt que de les gérer en « standalone ». Aujourd’hui, VSCT a choisi de s’inscrire dans cette optique par le biais du lancement de ses applications « NextGen ».

Pour finir, nous n’en sommes aujourd’hui qu’au début de cette nouvelle génération du langage Java. Ce dernier demeure en mode expérimentation : le décollage s’effectue donc en douceur et le code reste toujours rétro-compatible avec les anciennes versions. Ce nouveau mode de programmation et les langages fonctionnels semblent être la nouvelle orientation à prendre. On risque cependant d’attendre longtemps avoir de le voir généralisé sur tout le « Core Java ». En attendant, voyons ce que donnera le projet « Jigsaw » sur la version 9.

Share
Nadhem LAMTI
Nadhem LAMTI

3004

Comments

  1. Excellent tutoriel

Leave a Reply

Your email address will not be published. Required fields are marked *