Découvrir Kotlin en migrant une webapp Spring Boot

Lors la dernière conférence Google I/O qui s’est tenue en mai 2017, Google a officialisé le support de Kotlin sur Android. Google n’est pas le seul acteur de l’IT à miser sur ce nouveau langage créé par JetBrains (l’éditeur de l’IDE IntelliJ) et s’exécutant sur la JVM (mais pas que). En effet, dès février 2016, Pivotal proposait de développer des applications Spring Boot avec Kotlin. En janvier 2017, ils annonçaient que la version 5 du framework Spring proposerait des fonctionnalités exclusives à Kotlin. Chez Gradle, le langage Kotlin est désormais privilégié au détriment de Groovy.

Pour découvrir ce nouveau venu dans la galaxie des langages de programmation, je me suis intéressé à migrer vers Kotlin l’application démo Spring Petclinic développée en Java et Spring Boot. Je souhaitais ici partager son code source : spring-petclinic-kotlin et énumérer les différences notables avec sa version Java.

Une migration en souplesse

En m’appuyant sur le manuel de référence de Kotlin, j’ai pu migrer l’application sans trop de difficulté et en quelques heures. IntelliJ m’a grandement facilité la tâche puisqu’un copier/coller d’une classe Java dans un fichier Kotlin (extension .kt) lançait le plugin de conversion automatique. Quelques adaptations manuelles restaient néanmoins nécessaires.

Grâce à l’interopérabilité de Kotlin avec Java, j’ai pu faire cohabiter classes Kotlin et classes Java dans le même projet IntelliJ. Au cours de la migration, cela m’a permis de vérifier régulièrement le bon fonctionnement l’application.

Des conventions qui changent

Kotlin changent certaines conventions du langage Java :

1. Les classes et les méthodes sont par défaut finales et ne peuvent être héritées / redéfinies sans l’utilisation du mot clé open
Dans Petclinic, la classe BaseEntity parente de toutes les entités JPA est déclarée ainsi :

@MappedSuperclass
open class BaseEntity

L’omission du paramètre open déclenche une erreur de compilation des classes filles : « This type is final, so it cannot be inherited from ».

Ce changement de comportement impacte le fonctionnement de certaines librairies tierces. En effet, lors de l’utilisation d’annotations tels que @Cacheable ou @Configuration, le framework Spring utilise l’héritage pour instrumenter le code. La configuration du plugin Spring pour le compilateur Kotlin dans le pom.xml permet de s’affranchir de l’ajout du mot clé open sur les beans Spring de type @Component.

2. La visibilité des méthodes et des classes est par défaut publique
Appartenant au package visit, la classe Visit est référencée par la classe Pet du package de même niveau owner :

class Visit : BaseEntity()

3. Les types primitifs de Java disparaissent. Plus besoin de choisir entre un int et un Integer : vous utiliserez un Int.

4. Le type des variables et de retour de méthode n’est plus déclaré à gauche mais à droite.
Extrait de l’interface OwnerRepository :

fun findById(@Param(« id ») id: Int): Owner

Il faut s’y faire et retrouver ses habitudes du bon vieux Turbo Pascal.

5. Par défaut, aucune variable ne peut être null. Le compilateur vous rappellera à l’ordre. Lorsqu’une variable peut prendre la valeur null, il est nécessaire de le préciser explicitement en faisant suivre son type par le caractère ?

var name: String? = null

6. Les getter/setter (mutateurs) des propriétés d’une classe sont générés automatiquement par Kotlin. Dans le code, on accède directement à une propriété sans passer par les mutateurs. Kotlin ajoute automatiquement l’appel au mutateur correspondant.
Là ou en Java on passait par un setter :

james.setLastName("Carter");

en Kotlin, on affecte directement la valeur à la propriété :

james.lastName = "Carter"

Bien entendu, Kotlin offre la possibilité de ne générer qu’un des 2 mutateurs et/ou de redéfinir leur implémentation. Par exemple, dans la BaseEntity.kt, la propriété isNew est évaluée à partie de l’ID de l’entité :

val isNew: Boolean
    get() = this.id == null

Une syntaxe allégée

Par rapport à Java, Kotlin se veut apporter de la concision sans perdre en lisibilité, et ceci par le biais de léger changements syntaxiques.

1. Le signe point-virgule ; en fin d’instruction devient facultatif. Et lorsqu’une méthode ne comporte qu’une seule instruction, l’utilisation d’accolades et du mot clé return ne sont plus nécessaires.
Extrait du PetController Java :

@ModelAttribute("types")
public Collection<PetType> populatePetTypes() {
    return this.pets.findPetTypes();
}

Extrait du PetController Kotlin :

@ModelAttribute("types")
fun populatePetTypes(): Collection<PetType> = this.pets.findPetTypes()

2. Concernant l’héritage et l’implémentation d’une interface, les mots clés extends et implements sont remplacés par le symbole :

Code Java :

public interface VetRepository extends Repository<Vet, Integer> {

Code Kotlin :

interface VetRepository : Repository<Vet, Int> {

3. Le compilateur Kotlin sait inférer le type des variables. Lorsque vous déclarez une variable en lui affectant une valeur (autre que null), il n’est plus nécessaire de spécifier son type.
Exemple issu de Owner.kt :

@Column(name = "city")
@NotEmpty
var city = ""

4. L’instruction for each permettant d’itérer sur les éléments d’une collection change de syntaxe. Kotlin passe du : au in. A noter que le type de variable n’est plus exigé.

Version Java :

for (Pet pet : getPetsInternal()) {

Version Kotlin :

for (pet in pets) {

Des améliorations intéressantes

La plus-value de Kotlin par rapport à Java dépasse les conventions et les changements syntaxiques évoqués dans les 2 paragraphes précédents.

1. Kotlin propose de créer automatiquement des POJO avec getters, setters, méthodes equals(), hashCode(), toString() et copy() (cette dernière étant propre à Kotlin) via un mécanisme appelé data class.
La classe Vets profite de cette simplification :

@XmlRootElement
data class Vets(var vetList: Collection<Vet>? = null)

2. Dans les contrôleurs Spring MVC écrits en Java, il est courant d’avoir une suite de conditions if else dont chaque bloc renvoie sur une page différente.
Extrait de la méthode processFindForm de la classe Java OwnerController:

if (results.isEmpty()) {
    result.rejectValue("lastName", "notFound", "not found");
    return "owners/findOwners";
} else if (results.size() == 1) {
    owner = results.iterator().next();
    return "redirect:/owners/" + owner.getId();
} else {
    model.put("selections", results);
    return "owners/ownersList";
}

Pour réduire le nombre de return, Kotlin permet d’utiliser le if comme expression et non plus comme instruction. Lorsqu’une branche contient plusieurs instructions, la dernière est assignée au if ; dans l’exemple ci-dessous, c’est le nom de la page :

return if (results.isEmpty()) {
    result.rejectValue("lastName", "notFound", "not found")
    "owners/findOwners"
} else if (results.size == 1) {
    val foundOwner = results.iterator().next();
    "redirect:/owners/" + foundOwner.id
} else {
    model.put("selections", results)
    "owners/ownersList"
}

Autant dire que Kotlin sait faire plaisir à SonarQube en limitant l’usage de l’instruction return.

Une autre façon d’écrire ce code consiste à utiliser l’expression when qui est une sorte de super switch case. Dans la classe OwnerController Kotlin, les if / else disparaissent au profit de lambdas :

return when {
    results.isEmpty() -> {
        result.rejectValue("lastName", "notFound", "not found")
        "owners/findOwners"
    }
    results.size == 1 -> {
       "redirect:/owners/" + results.first().id    }
    else -> {
        model.put("selections", results)
        "owners/ownersList"
    }
}

3. Compatible avec Java 6, Kotlin avait introduit les lambda avant Java 8. Les collections ont été enrichies de méthodes permettant d’itérer, de filtrer, de trier, trouver un élément, récupérer le dernier … On peut y accéder directement, à savoir passer par un stream.

Extrait de la classe Owner codée en Java 6 :

public List<Pet> getPets() {
    List<Pet> sortedPets = new ArrayList<>(getPetsInternal());
    PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true));
    return Collections.unmodifiableList(sortedPets);
}

Pendant en Kotlin :

fun getPets(): List<Pet> =
        pets.sortedWith(compareBy({ it.name }))

La méthode find() permet de rechercher un élément dans une collection. A noter l’utilisation de l’opérateur ?: qui permet de lever une exception si find renvoie null.
Extrait de la classe PetTypeFormatter.kt :

findPetTypes.find { it.name == text } ?:
            throw ParseException("type not found: " + text, 0)

4. Bien que par défaut les variables ne puissent être null, nous avons vu qu’il était possible de les rendre nullable. L’opérateur elvis ?. permet d’accéder à des propriétés sans craindre des NullPointerException :

val compName = pet.name?.toLowerCase()

Kotlin proposent d’autres fonctionnalités fortes intéressantes que je n’ai pas eu l’occasion de mettre en œuvre dans Spring Petclinic. Je pense notamment aux extension functions qui permettent d’ajouter dynamiquement des méthodes à une classe.

Des changements plus discutables

1. La déclaration de constantes ne passe plus par l’usage des mots clés static final devant la propriété d’une classe. A la place, Kotlin propose de passer par des constantes de portée globale ou par des objets companion.

Constantes globales (extrait de PetControllerTest.kt) :

const val TEST_OWNER_ID = 1
const val TEST_PET_ID = 1

Constantes internes à une classe (extrait de PetValidator.kt) :

companion object {
    const val TEST_PET_ID = 1
}

2. Un développeur Spring et JPA utilise massivement les annotations. Or, lorsque la propriété est multi-valuée (tableau), Kotlin requière l’utilisation du mot clé arrayOf
Exemple d’un mapping @OneToMany JPA extrait de Owner.kt :

@OneToMany(cascade = arrayOf(CascadeType.ALL), mappedBy = "owner")
var pets: MutableSet<Pet> = HashSet()

Pour le coup, on perd en lisibilité par rapport à Java. Heureusement, ce désagrément devrait être corrigé dans une prochaine version de Kotlin : KT-11235

Conclusion

Pour un développeur Java, l’apprentissage de Kotlin se fera sans trop d’effort.
Sans révolutionner Java, Kotlin permet de moderniser sa syntaxe. Il apporte quelques nouveautés fortes appréciables une fois qu’on y a goûté.

Débutant sur Kotlin, je suis preneur de toute suggestion d’amélioration (et il doit y en avoir !!). Tout contributeur est le bienvenu.

Pour aller jusqu’au bout de l’exercice, il serait intéressant de migrer le build Maven vers un build Gradle écrit en Kotlin (réf. #2). Là encore, avis aux amateurs.

Ressources :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.