I. 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 toute la logique 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 dix ans pour moi !) et si le passage à Java 8 peut s'effectuer sans difficulté 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 ».
II. 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'hommes/jours supplémentaires, 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.
III. Étape 1 : constitution 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 cinq mois grâce au développement de miniexemples, 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 préalable de Java 8.
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 huit classes simples, huit classes moyennes et huit classes complexes réparties sur trois projets distincts. Leur attribution s'est faite de manière aléatoire, ce qui nous a permis de dresser troistableaux que nous analyserons plus bas.
IV. Étape 2 : migration et exécution de tests unitaires▲
Une fois les rôles définis et les classes identifiées, nous avons commencé 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 : 2 h 30] ;
- présentation de Java 8 axée autour des grandes nouveautés et des best practices à prendre en compte [durée : 1 h 30] ;
- 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 » :
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 trois tableaux ci-dessous. La ligne Équipe 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).
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.
IV-A. Exemple 1▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
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 Java 8
// 20 m.
return
fids.stream
(
)
.mapToInt
(
fid ->
Integer.valueOf
(
fid.getCode
(
)))
.mapToObj
(
FrequentTravellerCardTypeEnum::getById)
.peek
(
e->
{
if
(
e.getCode
(
)) logCarteInconnu
(
e);
}
).filter
(
e ->
e !=
null
);
}
IV-B. Exemple 2▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
/* 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 m.
PaymentCard carteARendrePrincipale =
lesCartesDePaiement.stream
(
)
.filter
(
carte->
!
carte.isExpired
(
) &&
!
carte.isDefault
(
))
.min
(
Comparator.comparing
(
carte->
carte.getCreationDate
(
).getTime
(
)))
.orElse
(
null
);
IV-C. Exemple 3▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
/*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é 1 h 30 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
(
);
}
V. Étape 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 percentiles et le temps de latence maximum de chaque test.
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ées.
« 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 ».
Comme vous pouvez le constater, les performances du code Java 8 sont deux fois meilleures sur ce « StressTest ».
Contrairement à la première illustration, sur ce test le code Java 8 est deux fois moins performant. Conclusion : le code est à revoir !
VI. 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 (1 h /méthode) pour les classes moyennes et, au minimum, au triple (<1 h 30/méthode) pour les classes complexes.
Une connaissance plus approfondie sur les pratiques avancées des nouvelles API 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.
VII. 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éressant : 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 « Refactorings » 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 Java 8/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étier, 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étrocompatible 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 avant de le voir généralisé sur tout le « Core Java ». En attendant, voyons ce que donnera le projet « Jigsaw » sur la version 9.
VIII. Remerciements▲
Cet article a été publié avec l'aimable autorisation de la société PaloITPaloIT.
Nous tenons à remercier Claude Leloup pour sa relecture orthographique et Mickael Baron pour la mise au gabarit de cet article.