Blog Arolla

kotlin et java sont dans un module

J'adore kotlin. Notamment parce qu'il permet de migrer doucement depuis java. En effet, on peut faire cohabiter java et kotlin dans le même module maven (ou gradle). L'un peut appeler lautre, qui peut appeler l'un, et inversement. Si on ajoute à ça la facilité qu'offre intellij pour migrer un fichier java en kotlin, on exulte et on migre sans se poser de questions. Sauf dans certains cas.

Pour illustrer ça, je suis parti du kata birthday greetings de Matteo Vaccari. Je l'ai rapidement refactoré pour aller vers une architecture hexagonale simpliste. N'y cherchez pas les choses qui restent à faire, il y en a. J'en ai fait juste assez pour illustrer mon propos. J'obtiens quelque chose comme ça, avec les dépendances run-time suivantes :

dépendances runtime

Dans ce code, je veux convertir CsvEmployeeRepository en kotlin pour le faire évoluer. Si je devais le sortir dans un autre module kotlin, qui peut être appelé par le module java existant, je devrais aussi y déplacer la classe Employee. Mais je n'ai pas besoin de le faire, parce que je peux faire cohabiter kotlin et java dans le même module. Le java peut ainsi appeler kotlin, et vice-versa.

Pour cela, j'utilise kotlin-maven-plugin, en suivant bêtement la doc de kotlin. J'ajoute à mon pom une property indiquant la version de kotlin à utiliser, la dépendance sur la stdlib kotlin, et le plugin configuré pour faire cohabiter java et kotlin. Ça me donne les ajouts suivants :

<project…>
    …
    <properties>
        …
        <kotlin.version>1.7.21</kotlin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
        …
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
                                <sourceDir>${project.basedir}/src/main/java</sourceDir>
                            </sourceDirs>
                        </configuration>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
                                <sourceDir>${project.basedir}/src/test/java</sourceDir>
                            </sourceDirs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <executions>
                    <!-- Replacing default-compile as it is treated specially by maven -->
                    <execution>
                        <id>default-compile</id>
                        <phase>none</phase>
                    </execution>
                    <!-- Replacing default-testCompile as it is treated specially by maven -->
                    <execution>
                        <id>default-testCompile</id>
                        <phase>none</phase>
                    </execution>
                    <execution>
                        <id>java-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>java-test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

C'est gros, mais je me suis contenté de copier/coller la doc, et je ne me souviens pas avoir eu besoin de maintenir la configuration de ce plugin dans un projet passé.

Ensuite j'invoque intellij pour convertir CsvEmployeeRepository en kotlin. L'IDE propose une fonction « Convert File to kotlin » pour ça (Ctrl+Alt+Shift+K). Et c'est tout. Enfin, dans ce cas où le code java était plus tordu qu'il n'y paraissait, il a fallu faire 2 modifications manuelles une fois converti : déclarer str nullable et ajouter un !! pour signaler qu'il ne peut pas être null dans la boucle :

Conversion manuelle

J'aurais pu laisser CsvEmployeeRepository dans le répertoire src/main/java, mais par souci de cohérence, je l'ai déplacé dans un nouveau répertoire src/main/kotlin. Dans le cadre d'une migration, ça aide à voir où on en est, et ça satisfait mes besoins d'ordre psychiatrique.

arborescence

Les tests et le main n'ont jamais cessé de fonctionner pendant ce refactoring.

Ah, je vous avais parlé de limites à cette approche. Effectivement, il y en a au moins une, et de taille. Si le code d'origine avait nécessité une phase de génération de code en précompilation, je n'aurais pas pu faire ça. En particulier, si on a du lombok dans le java d'origine, ça ne peut pas marcher. En effet, dans ce cas, maven ne sait pas dans quel ordre exécuter la précompilation lombok, et les compilations java et kotlin. Il y a probablement une boucle infinie à résoudre pour trouver cet ordre. Je n'ai pas assez creusé le sujet pour en comprendre tous les détails, et j'espère voir des explications plus abouties en retour.

C'est d'autant plus dommage qu'on utilise lombok et kotlin dans la même optique au départ : diminuer le bruit du java dans le code. Dans un code de l'ère pré-kotlinienne, on peut avoir choisi d'utiliser lombok pour les mêmes raisons qui nous pousseraient aujourd'hui à passer sur du kotlin. Et là, je ne vois pas de chemin fluide pour passer de l'un à l'autre. Quelques pistes pour y arriver tout de même :

  • delombok, évidemment. Intellij propose des fonctions pour retirer lombok du code. Il se charge de remettre l'équivalent du code généré dans les classes qui contenaient du lombok. Et ça marche plutôt bien.
  • On peut séparer des modules java et kotlin. Ça suppose de casser les dépendances qui traversent les 2 langages pour éviter les dépendances circulaires entre les modules. Par ex, dans notre cas, BirthdayService en java qui appelle CsvEmployeeRepository en kotlin qui appelle Employee en java (et qui hérite d'EmployeeRepository en java) n'est pas si simple à séparer en modules par langage. Ça suppose soit de convertir aussi Employee (et EmployeeRepository) pour l'envoyer dans le module kotlin, soit d'extraire un autre module java (par ex model) pour Employee et EmployeeRepository. Quoi qu'il en soit, la marche est tout de suite plus haute que le refactoring progressif décrit ci-dessus.
  • Peut-être que gradle aiderait à se sortir de la boucle infinie, je ne sais pas. Même si c'était possible, ça supposerait une migration gradle, ce qui est aussi une énorme tâche.

A vous de jouer. Essayez, vous verrez, c'est surprenant. Ne restent plus que les réticences d'usage et la politique interne pour évoluer tout en douceur vers un langage un peu plus sucré que le java. J'attends avec impatience vos explications et propositions concernant la limitation de migration depuis lombok vers kotlin.

Plus de publications

2 comments for “kotlin et java sont dans un module

  1. 16 janvier 2023 at 19 h 25 min

    Vive Kotlin ! (Et Java, par la même occasion).

    J’ai commencé à développer en Java pendant ma formation, et j’ai rapidement évolué sur Kotlin, c’est difficile de revenir sur du Java après avoir adopté Kotlin ..

  2. Antoine Alberti
    27 janvier 2023 at 11 h 22 min

    Merci pour le commentaire.
    Effectivement java peut être frustrant après kotlin. Ce qui a fait, et continue à faire, son succès, ralentit le langage. Je comprends pourquoi, mais ça reste frustrant. Kotlin est un compromis inespéré pour moderniser une base java. Et jusqu’à aujourd’hui, je n’ai jamais entendu de bon argument pour ne pas tenter de tremper un orteil dans le kotlin. Et pourtant j’ai vu un paquet d’arguments pour rester en java.