Les nouveautés de Groovy 2.0
La sortie de Groovy 2.0 apporte des fonctionnalités, clés au langage, telles que la vérification statique des types et la compilation statique. Elle adopte les évolutions liées au JDK7 avec, entre autres, les améliorations syntaxiques de Project Coin et le support de la nouvelle instruction JVM "invoke dynamic". De plus, Groovy 2.0 devient plus modulaire qu’auparavant. Dans cet article, nous allons nous intéresser, en détail, à ces nouvelles fonctionnalités.
Un "thème statique" pour un langage dynamique
Vérification statique des types
De par sa nature, Groovy est et restera toujours un langage dynamique. Cependant, il est souvent utilisé comme un "langage de script Java", ou simplement comme un "Java enrichi" (c.-à-d., un Java avec moins de redondances syntaxiques et plus de fonctionnalités). En réalité, de nombreux développeurs utilisent et embarquent Groovy dans leurs applications Java comme une extension du langage; dans le but d'écrire des règles métiers plus compréhensibles et de personnaliser davantage l'application pour différents clients, etc. Pour ces cas d'utilisations, les développeurs n'ont pas besoin de toutes les capacités dynamiques offertes par le langage et s'attendent généralement aux mêmes retours du compilateur Groovy que de javac. Dans la majorité des cas, ils espèrent obtenir des erreurs de compilations (plutôt que des erreurs d'exécution) lors d'éventuelles erreurs de frappe sur le nom des variables ou des méthodes, ou lors d'assignations de types incorrects, etc. C’est pourquoi Groovy2 dispose du support pour la vérification statique des types.
Détecter les fautes de frappe évidentes
La vérification statique des types est basée sur l'utilisation de puissants mécanismes de transformation AST (Abstract Syntax Tree) existants dans Groovy. Pour ceux qui ne sont pas familiarisés avec ces mécanismes, vous pouvez voir cela comme un plugin optionnel du compilateur, déclenché par une annotation. Étant une fonctionnalité optionnelle, vous n'êtes pas obligé de l'utiliser si vous n'en avez pas besoin. Pour déclencher la vérification statique, utiliser simplement l'annotation @TypeChecked sur la déclaration d'une méthode ou d'une classe en fonction du niveau de granularité désiré.
Mettons cela en action avec un premier exemple:
import groovy.transform.TypeChecked
void someMethod() {}
@TypeChecked
void test() {
// compilation error:
// cannot find matching method sommeeMethod()
sommeeMethod()
def name = "Marion"
// compilation error:
// the variable naaammme is undeclared
println naaammme
}
Nous avons annoté la méthode test() avec @TypeChecked, pour indiquer au compilateur Groovy d'effectuer, à la compilation, la vérification statique spécifiquement pour cette méthode. Nous essayons ensuite, d'appeler la méthode someMethod(), avec une faute de frappe évidente, et d'afficher la variable name (ici encore avec une autre faute de frappe). Le compilateur lèvera en conséquence deux erreurs de compilation, respectivement pour la méthode et la variable non trouvées ou non déclarées.
Vérifier vos déclarations et vos valeurs de retour
Le vérificateur statique contrôle aussi que les types et les valeurs retournés de vos assignations sont cohérents:
import groovy.transform.TypeChecked
@TypeChecked
Date test() {
// compilation error:
// cannot assign value of Date
// to variable of type int
int object = new Date()
String[] letters = ['a', 'b', 'c']
// compilation error:
// cannot assign value of type String
// to variable of type Date
Date aDateVariable = letters[0]
// compilation error:
// cannot return value of type String
// on method returning type Date
return "today"
}
Dans cet exemple, le compilateur se plaindra à propos du fait que vous ne pouvez pas assigner une Date à une variable de type int
, de même vous ne pouvez pas retourner une String
à la place du type Date
, spécifié en signature de la méthode. La deuxième erreur de compilation est également intéressante, car non seulement elle soulève la mauvaise affectation, mais aussi car elle montre l'inférence de type réalisée, puisque le compilateur sait que la valeur de letters[0]
est de type String
, en effet, nous travaillons avec un tableau de Strings.
Plus de détails sur l'inférence de type
Comme nous parlons d'inférence de type, jetons un œil sur ses autres occurrences. Nous avons dit que la vérification statique contrôle aussi les types et les valeurs de retour:
import groovy.transform.TypeChecked
@TypeChecked
int method() {
if (true) {
// compilation error:
// cannot return value of type String
// on method returning type int
'String'
} else {
42
}
}
Prenons une méthode retournant une valeur de type primitif int, la vérification statique est aussi capable de vérifier les valeurs retournées depuis différentes structures telles que if/else, try/catch ou switch/case. Dans notre exemple, la structure if/else essaye de retourner une String à la place d'une valeur de type primitif int, le compilateur retournera alors une erreur.
Les conversions de type simple restent possibles
Le vérificateur statique des types ne soulèvera, cependant, pas d'erreur pour certaines des conversions automatiques supportées par Groovy. Dans l'exemple suivant, Groovy converti automatiquement les valeurs de retour en fonction des signatures des méthodes suivantes, retournant une String
, un boolean
ou une Class
:
import groovy.transform.TypeChecked
@TypeChecked
boolean booleanMethod() {
"non empty strings are evaluated to true"
}
assert booleanMethod() == true
@TypeChecked
String stringMethod() {
// StringBuilder converted to String calling toString()
new StringBuilder() << "non empty string"
}
assert stringMethod() instanceof String
@TypeChecked
Class classMethod() {
// the java.util.List class will be returned
"java.util.List"
}
assert classMethod() == List
Le vérificateur statique des types est aussi assez sophistiqué pour faire de l’inférence de type:
import groovy.transform.TypeChecked
@TypeChecked
void method() {
def name = " Guillaume "
// String type inferred (even inside GString)
println "NAME = ${name.toUpperCase()}"
// Groovy GDK method support
// (GDK operator overloading too)
println name.trim()
int[] numbers = [1, 2, 3]
// Element n is an int
for (int n in numbers) {
println
}
}
Bien que la variable name soit définie avec le mot clé def
, le vérificateur de type déduira qu’il s’agit d’un type String
. Ainsi, quand la variable est utilisée dans la chaîne interpolée, il saura que la méthode toUpperCase()
ou trim()
peut être appelée. Notez que cette dernière est ajoutée par le Groovy Development Kit en décorant la classe String
. Finalement, en itérant sur les éléments d'un tableau de type primitif int
, il infère aussi qu'un élément de ce tableau est évidemment de type int
.
Mélanger les aspects dynamiques et les méthodes statiquement typées
Il faut garder à l’esprit que l’utilisation de la vérification statique des types restreint les possibilités offertes par Groovy. La plupart des fonctions dynamiques, normalement disponibles à l’exécution, ne sont plus permises en raison du fait que leurs types ne peuvent pas être vérifiés statiquement lors de la compilation. Ainsi, l’ajout d’une nouvelle méthode à l’exécution, à travers les metaclasses, n’est pas possible. Cependant, lorsque vous avez besoin d’utiliser certaines fonctionnalités dynamiques, telles que les builders de Groovy, vous pouvez choisir d'exclure la vérification statique que vous souhaitez.
L’annotation @TypeChecked
peut être utilisée au niveau d’une classe, si vous désirez avoir une vérification statique sur son ensemble. De plus, vous pouvez placer l’annotation seulement au niveau des méthodes que vous voulez voir statiquement typées. Enfin, si vous désirez avoir toute votre classe statiquement typée à l’exception d’une méthode, vous pouvez annoter cette dernière avec @TypeChecked(TypeCheckingMode.SKIP)
- ou @TypeChecked(SKIP)
si vous avez statiquement importé l’enum associée.
Illustrons cette situation avec le script suivant, dans lequel la méthode greeting()
est statiquement typée tandis que la méthode generateMarkup()
ne l’est pas.
import groovy.transform.TypeChecked
import groovy.xml.MarkupBuilder
// this method and its code are type checked
@TypeChecked
String greeting(String name) {
generateMarkup(name.toUpperCase())
}
// this method isn't type checked
// and you can use dynamic features like the markup builder
String generateMarkup(String name) {
def sw =new StringWriter()
new MarkupBuilder(sw).html {
body {
div name
}
}
sw.toString()
}
assert greeting("Cédric").contains("<div>CÉDRIC</div>")
L'inférence de types avec instanceof
Les versions de Java, actuellement en production, ne supportent pas l’inférence de type; alors que nous trouvons aujourd’hui de nombreuses situations où le code est souvent verbeux et encombré d’instructions redondantes. Cela obscurcit la signification du code et sans l'utilisation de puissants IDEs il devient plus difficile de développer. On retrouve ce problème avec l'utilisation de l'instruction instanceof: la plupart du temps, vous l'utilisée à l’intérieur d’une condition if, pour tester la classe d’une valeur et vous êtes ensuite obligés de caster cette valeur pour utiliser les méthodes associées à son type. En pur Groovy, avec le mode de vérification statique des types, vous pouvez complètement vous débarrasser de ces casts.
import groovy.transform.TypeChecked
import groovy.xml.MarkupBuilder
@TypeChecked
String test(Object val) {
if (val instanceof String) {
// unlike Java:
// return ((String)val).toUpperCase()
val.toUpperCase()
} else if (val instanceof Number) {
// unlike Java:
// return ((Number)val).intValue().multiply(2)
val.intValue() * 2
}
}
assert test('abc') == 'ABC'
assert test(123) == '246'
Dans l’exemple ci-dessus, le vérificateur infère que l’objet val est de type String
à l’intérieur du bloc if
et de type Number
dans le bloc else if
, sans qu'il soit nécessairement casté.
La plus petite limite supérieure
Le vérificateur statique des types va un peu plus loin en termes d’inférence, dans le sens où il a une compréhension plus fine du type de vos objets. Prenons le code suivant:
import groovy.transform.TypeChecked
// inferred return type:
// a list of numbers which are comparable and serializable
@TypeChecked test() {
// an integer and a BigDecimal
return [1234, 3.14]
}
Dans cet exemple, nous retournons, instinctivement, une liste de nombres : un Integer
et un BigDecimal
. Cependant, le vérificateur statique de types calcule ce que nous appelons « la plus petite limite supérieure », c'est à dire, en réalité, une liste de nombres à la fois sérialisables et comparables. Il n’est pas possible d’indiquer ces types avec la notation Java standard, mais si nous avions une sorte d’opérande tel que ‘&’, la définition de la liste pourrait être de la forme suivante: List<Number & Serializable & Comparable>
.
Typage continu (Flow typing)
Bien que cela ne soit pas recommandé comme une bonne pratique, les développeurs utilisent de temps à autre une même variable, non typée, pour stocker des valeurs de différents types. En considérant le code ci-dessous :
import groovy.transform.TypeChecked
@TypeChecked test() {
def var = 123 // inferred type is int
var = "123" // assign var with a String
println var.toInteger() // no problem, no need to cast
var = 123
println var.toUpperCase() // error, var is int!
}
La variable var est initialisée avec un int puis un String. L’algorithme de “typage continu” suit les assignations successives et comprend ainsi que la variable est maintenant de type String
. Le vérificateur statique de types ne soulèvera pas d’erreur sur l’appel de la méthode toInteger()
, ajoutée par Groovy à la classe String
. Ensuite, la variable est à nouveau initialisée avec un entier, mais cette fois-ci, le vérificateur de type lèvera une erreur de compilation lors de l’appel de la méthode toUpperCase()
sur un Integer
.
Il y a des cas spéciaux d’utilisation de l’algorithme de typage continu particulièrement intéressants lorsqu'une variable est partagée avec une closure. Que ce passe-t-il lorsqu’une variable locale est référencée dans une closure à l’intérieur de la méthode ou cette variable est définie ? Regardons l’exemple suivant:
import groovy.transform.TypeChecked
@TypeChecked test() {
def var = "abc"
def cl = {
if (new Random().nextBoolean()) var = new Date()
}
cl()
var.toUpperCase() // compilation error!
}
La variable locale var
est assignée à une String
, mais ensuite, elle peut être assignée à un objet Date
si une variable aléatoire est à true. Typiquement, ce n'est qu'à l’exécution que nous saurons réellement si la condition if, présente dans la closure, est vérifiée ou non. À la compilation, il n’y aura donc aucune chance pour le compilateur de savoir si var contient une String ou une Date. C’est pourquoi, il lèvera une erreur lors de l’appel à la méthode toUpperCase(), puisqu’il n’est pas possible pour lui d’inférer le type réel de la variable. Cet exemple est quelque peu simpliste mais montre cependant un cas intéressant :
import groovy.transform.TypeChecked
class A { void foo() {} }
class B extends A { void bar() {} }
@TypeChecked test() {
def var = new A()
def cl = { var = new B() }
cl()
// var is at least an instance of A
// so we are allowed to call method foo()
var.foo()
}
Dans la méthode test()
, ci-dessus, var
est assignée à une instance A
, puis à une instance B
dans la closure appelée à la suite. De ce fait, nous pouvons au minimum inférer que la variable var
est de type A
.
Toutes ces vérifications, ajoutées au compilateur Groovy, sont faites à la compilation, mais le bytecode généré reste le même code dynamique que d’habitude - il n’y a aucun changement dans le comportement.
Puisque le compilateur en connaît, maintenant, davantage sur le typage de votre code, il s’ouvre quelques possibilités intéressantes : pourquoi ne pas compiler statiquement le code dont le typage a été vérifié ? L’avantage évident est que le bytecode généré sera plus ressemblant au bytecode créé par le compilateur javac lui même, rendant, entre autres, le code Groovy statiquement compilé aussi rapide que si c’était du Java. Dans la partie suivante, nous allons en apprendre plus sur la compilation statique de Groovy.
Compilation statique
Comme nous le verrons dans le chapitre suivant, à propos de l’alignement sur le JDK7, Groovy 2.0 supporte la nouvelle instruction JVM "_invoke dynamic_" ainsi que ses APIs liées, facilitant le développement des langages dynamiques sur la plate-forme Java et apportant quelques performances additionnelles aux appels dynamiques de Groovy. Cependant, ou malheureusement, devrais-je dire, JDK 7 n’est pas massivement déployé en production au moment où j’écris ces lignes, tout le monde n’a ainsi pas la chance de tourner sur la dernière version. Les développeurs à la recherche d’améliorations de performances ne verront aucun changement avec groovy 2.0, s’ils ne sont pas en mesure de passer au JDK 7. Heureusement, l’équipe de développement Groovy a pensé que ces développeurs pourraient être intéressés par un gain de performance s’ils pouvaient, parmi d’autres avantages, permettre au code dont le typage a été vérifié d’être compilé statiquement.
Sans plus tarder, plongeons maintenant dans l'utilisation de la nouvelle annotation @CompileStatic
:
import groovy.transform.CompileStatic
@CompileStatic
int squarePlusOne(int num) {
num * num + 1
}
assert squarePlusOne(3) == 10
Cette fois-ci, au lieu d’utiliser @TypeChecked
, utilisez @CompileStatic
et votre code sera compilé statiquement. Le bytecode généré ressemblera au bytecode de javac et s’exécutera aussi vite. Comme l’annotation @TypeChecked
, @CompileStatic
peut annoter des classes et des méthodes ; et @CompileStatic(SKIP)
peut exclure la compilation statique pour une méthode spécifique lorsque sa classe est annotée avec @CompileStatic.
Un autre avantage de générer du bytecode proche de javac est que celui-ci aura une taille plus petite que celui généralement généré par Groovy pour les méthodes dynamiques. En effet, pour supporter les fonctionnalités dynamiques de Groovy, le bytecode contient des instructions additionnelles à appeler dans le système d’exécution de Groovy.
En ce qui concerne le dernier avantage, mais non le moindre, la compilation statique peut être utilisée par les développeurs de frameworks ou de librairies pour éviter les interactions indésirables lors de l'utilisation de la méta-programmation dynamique dans plusieurs parties du code. Les fonctionnalités dynamiques disponibles dans les langages tels que Groovy donnent une force et une flexibilité incroyable aux développeurs. Cependant, si aucune précaution n’est prise, diverses suppositions peuvent exister, en différents endroits du système, sur les fonctionnalités de méta-programmation actives. Cela peut entraîner des conséquences inattendues. Par exemple, pensez à ce qui se passerait si vous utilisiez deux librairies qui ajoutent chacune une méthode de même nom, mais implémentée de manière différente, à l'une de vos classes. À quel comportement devrions-nous nous attendre? Les utilisateurs expérimentés, avec les langages dynamiques verront ce problème arriver bien avant et en auront entendu parler en tant que “monkey patching”. Être capable de compiler statiquement des parties de votre code - n’ayant pas besoin de fonctionnalités dynamiques - vous protège des problèmes de type “monkey patching”, puisque le code compilé statiquement ne passe pas dans le système d’exécution dynamique de Groovy. Bien que l’aspect d’exécution dynamique du langage n’est pas permis dans un contexte de compilation statique, tous les mécanismes habituels de transformation AST fonctionnent comme auparavant, puisque la plupart des transformations AST appliquent leur magie au moment de la compilation.
D’un point de vue performance, le code Groovy compilé statiquement est généralement plus ou moins aussi rapide que celui de javac. Pour les quelques micro-benchmarks que l’équipe de développement utilise, les performances sont identiques dans la plupart des cas et sont de temps à autre un peu plus lentes.
Historiquement, grâce à l'intégration transparente et uniforme de Java et Groovy, nous avions l’habitude de conseiller aux développeurs d'optimiser certaines routines de la JVM pour des gains de performance supplémentaires; mais maintenant, avec cette option de compilation statique, ce n'est plus le cas et les personnes qui souhaitent développer leurs projets entièrement en Groovy peuvent le faire.
Java 7 et le thème JDK 7
La grammaire du langage de programmation Groovy dérive en réalité de la grammaire Java, mais Groovy apporte, bien évidemment, d’agréables raccourcis pour rendre les développeurs plus productifs. Pour les développeurs Java, cette familiarité a toujours été un argument de vente pour le projet et son adoption à grande échelle grâce, notamment, à sa courbe d'apprentissage constante. Nous nous attendions à ce que les présents et futurs utilisateurs de Groovy désirent bénéficier des quelques raffinements de la syntaxe offerte par Java 7, avec son extension "Project Coin".
Au-delà des aspects de syntaxe, le JDK7 apporte aussi d’intéressantes nouveautés à ses APIs, et pour la première fois, depuis longtemps, une nouvelle instruction bytecode appelée “invoke dynamic”, qui vise à aider les développeurs à implémenter leurs langages dynamiques plus facilement tout en bénéficiant de plus de performances.
Project Coin - améliorations de syntaxe
Depuis le premier jour (cela nous ramène en 2003, déjà!), Groovy a apporté plusieurs améliorations de syntaxe et des fonctionnalités au-dessus de Java. On peut penser, par exemple, aux closures mais aussi à la possibilité d’utiliser plus que des valeurs discrètes avec l’instruction switch/case
, alors que Java 7 ne permet que l’utilisation supplémentaire des Strings. Ainsi, certaines des améliorations de syntaxe du "Project Coin" , comme l'utilisation des Strings avec le l'instruction switch, sont déjà présentes dans Groovy. Cependant, quelques une de ces améliorations sont nouvelles, tels que les littéraux binaires, l’underscore dans les expressions numériques, ou le catch multiple; et Groovy les supporte. La seule omission faite des améliorations de Project Coin est le "try-with-resources", pour lequel Groovy offre déjà différentes solutions via l'API riche du kit de développement.
Les littéraux binaires
En Java 6 et avant, ainsi que dans Groovy, les nombres pouvaient être représentés en décimal, octal et hexadécimal. Avec Java 7 et Groovy 2, vous pouvez utiliser une notation binaire avec le préfixe "0b":
int x = 0b10101111
assert x == 175
byte aByte = 0b00100001
assert aByte == 33
int anInt = 0b1010000101000101
assert anInt == 41285
L’Underscore dans les littéraux numériques
Lors de l'écriture de longs nombres littéraux, il est difficile pour l’œil de comprendre comment certains nombres sont regroupés, par exemple avec des groupes de milliers, de mots, etc. En vous permettant de placer un séparateur dans les littéraux numériques, il est plus facile de repérer ces groupes:
long creditCardNumber = 1234_5678_9012_3456L
long socialSecurityNumbers = 999_99_9999L
double monetaryAmount = 12_345_132.12
long hexBytes = 0xFF_EC_DE_5E
long hexWords = 0xFFEC_DE5E
long maxLong = 0x7fff_ffff_ffff_ffffL
long alsoMaxLong = 9_223_372_036_854_775_807L
long bytes = 0b11010010_01101001_10010100_10010010
Catch multiple
Lors de la capture d'exceptions, nous répliquons souvent le bloc catch pour deux ou plusieurs exceptions que nous voulons traiter de la même manière. Une solution consiste soit à factoriser les éléments communs dans sa propre méthode, soit d'une façon moins élégante à avoir une approche fourre-tout en attrapant Exception, ou pire, Throwable. Avec le catch multiple, nous sommes en mesure de définir plusieurs exceptions à catcher et à traiter par un même bloc:
try {
/* ... */
} catch(IOException | NullPointerException e) {
/* one block to handle 2 exceptions */
}
Support de l'instruction "invoke dynamic"
Comme nous l'avons mentionné précédemment dans cet article, JDK7 est arrivé avec une nouvelle instruction bytecode appelée "invoke dynamic", ainsi qu'avec ses APIs associées. Leur but est d'aider les développeurs de langage dynamique dans leur travail d'élaboration de leurs langages au-dessus de la plate-forme Java. Pour cela, elle simplifie le câblage des appels de méthodes dynamiques, en définissant des « call sites » là où des sections d'appels de méthode dynamique peuvent être mises en cache, des "method handles" en tant que pointeurs de méthode, des "class values" pour stoker n'importe quel type de métadonnées sur des objets de classe, etc. Attention, en dépit des prometteuses améliorations de performances, "invoke dynamic" n'a pas encore été pleinement optimisée à l'intérieur de la JVM et ne délivre pas toujours les meilleures performances possibles, mais jour après jour, les optimisations arrivent.
Groovy apporte ses propres techniques d'implémentations, pour accélérer la sélection des méthodes et leurs invocations avec le "call site caching", pour stocker les méta-classes (celles équivalentes aux classes mais dynamiques pour l'exécution) avec son registre de méta-classes, pour effectuer des calculs natifs sur les primitifs aussi rapidement que Java, et bien plus. Mais avec l'avènement de «invoke dynamic», nous pouvons changer la base de l'implémentation de Groovy au-dessus de ces APIs et de cette instruction, pour obtenir des améliorations de performances et ainsi simplifier la base de notre de code.
Si vous avez la chance de travailler sous JDK 7, vous pourrez utiliser une nouvelle version des JARs Groovy qui ont été compilés avec le support « invoke dynamic». Ces JARs sont facilement reconnaissables car ils comportent "-indy" dans leurs noms.
Activation du support Invoke dynamic
L’utilisation des JARs "indy" n’est toutefois pas suffisante pour compiler votre code Groovy de façon à ce qu’il tire parti du support de "invoke dynamic". Pour cela, vous devrez utiliser le flag --indy avec le compilateur "groovyc" ou la commande "groovy". Cela signifie aussi que si vous utilisez les JARs "indy", vous pouvez toujours cibler le JDK5 ou 6 pour la compilation.
De même, si vous utilisez la tâche Ant groovyc pour compiler vos projets, vous pouvez aussi spécifier l’attribut indy:
...
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc"
classpathref="cp"/>
...
<groovyc srcdir="${srcDir}" destdir="${destDir}" indy="true">
<classpath>
...
</classpath>
</groovyc>
...
Le plugin de compilation Groovy Eclipse Maven n'a pas encore été mis à jour avec le support de Groovy 2.0, mais ce sera le cas prochainement. Pour les utilisateurs du plugin GMaven, bien qu’il soit déjà possible de configurer le plugin pour utiliser Groovy 2.0, il n'y a actuellement pas de flag pour activer le support invoke dynamic. Encore une fois, GMaven sera également bientôt mis à jour à cet égard.
Lors de l’intégration de Groovy dans vos applications Java, avec GroovyShell, par exemple, vous pouvez aussi activer le support en passant une instance de CompilerConfiguration au constructeur de GroovyShell, grâce à laquelle vous pouvez accéder et modifier les options d’optimisation:
CompilerConfiguration config = new CompilerConfiguration();
config.getOptimizationOptions().put("indy", true);
config.getOptimizationOptions().put("int", false);
GroovyShell shell = new GroovyShell(config);
Puisque l’invoke dynamic est supposé être un remplacement complet à l’envoi de méthodes dynamiques, il est aussi nécessaire de désactiver les optimisations primaires générant du bytecode supplémentaire, présent pour optimiser des cas extrêmes. Même si, dans certains cas, cela s’avère être plus lent qu’avec l’activation des optimisations primitives, les versions futures de la JVM implémenteront un JIT amélioré qui sera capable d’accorder la plupart des appels et de supprimer ceux inutiles.
Les améliorations de performances promises
Dans nos tests, nous avons constaté des gains de performance intéressants à certains endroits, alors que d'autres programmes peuvent fonctionner plus lentement avec l'utilisation du support invoke dynamic. Cependant, l'équipe Groovy a des améliorations de performances supplémentaires dans leur pipeline pour Groovy 2.1, mais nous avons remarqué que la JVM n'est pas encore affinée et a encore un long chemin à parcourir pour être totalement optimisée. Heureusement, les prochaines mises à jour du JDK 7 (en particulier la mise à jour 8) devraient déjà contenir ces améliorations, la situation ne peut donc que s'améliorer. En outre, comme invoke dynamic est utilisée pour la mise en œuvre des lambdas du JDK 8, nous pouvons être sûr que d'autres améliorations sont à venir.
Un Groovy plus modulaire
Nous finirons notre périple à travers les nouvelles fonctionnalités de Groovy 2.0 en parlant de modularité. Tout comme Java, Groovy n'est pas seulement un langage, mais c’est aussi un ensemble d'APIs servant à des fins diverses: templates, construction d’interfaces Swing, scripting Ant, intégration JMX, accès SQL, serveur de servlet, et plus encore. Les livrables de Groovy ont été construits avec l'ensemble de leurs fonctionnalités et de leurs APIs au sein d'un unique gros JAR. Cependant, tout le monde n’a pas besoin de tout, à tout moment, dans leurs propres applications: vous pourriez être intéressé par le moteur de templates et les servlets, si vous écrivez une application web, mais vous pourriez seulement avoir besoin du constructeur Swing lorsque vous travaillez sur une application client de bureau riche.
Les modules Groovy
Ainsi, le premier objectif de l'aspect modularité de cette version est de diviser réellement le JAR Groovy original en petits modules, en petits JARs. Le JAR de base Groovy est maintenant deux fois plus petit, et nous avons ces modules de fonction disponibles:
- Ant: pour scripter des tâches Ant dans le but d’automatiser les tâches d'administration
- BSF: pour intégrer Groovy à vos applications Java avec l’ancien framework Apache Bean Scripting
- Console: module contenant la console Swing de Groovy
- GroovyDoc: pour documenter vos classes Groovy et Java
- Groovysh: module correspondant au shell de ligne de commande Groovysh
- JMX: pour exposer et consommer des beans JMX
- JSON: pour produire et consommer des contenus JSON
- JSR-223: pour intégrer Groovy dans vos applications Java avec les APIs javax.scripting des JDK 6+
- Servlet: pour écrire et servir des servlets et des templates en Groovy
- SQL: pour interroger des bases de données relationnelles.
- Swing: pour construire des interfaces Swing
- Templates: pour utiliser le moteur de template
- Test: pour le support de tests, comme le GroovyTestCase, mocking, et plus
- TestNG: pour écrire dans Groovy des tests avec TestNG
- XML: pour produire et consommer des documents XML
Avec Groovy 2, vous êtes maintenant en mesure de simplement prendre les modules qui vous intéressent, plutôt que de tout inclure dans votre classpath. Cependant, nous fournissons toujours le "all" JAR qui contient tout, si vous ne voulez pas compliquer vos dépendances pour seulement quelques mégaoctets d'espace économisé. Nous fournissons également les JARs compilés avec le support "invoke dynamic", pour ceux qui utilisent le JDK 7.
Les modules d’extension
Le travail fait pour rendre Groovy plus modulaire a aussi apporté une nouvelle fonctionnalité intéressante : les modules d’extension. En effet, le fait d'avoir divisé Groovy en petits modules, a permis de créer un mécanisme permettant aux modules de contribuer à des méthodes d'extension. De cette façon, les modules d'extension peuvent fournir des méthodes d'instance et des méthodes statiques à d'autres classes, y compris à celles du JDK ou des librairies tierces. Groovy utilise ce mécanisme pour décorer les classes du JDK, afin d'ajouter de nouvelles méthodes utiles pour des classes comme String, File, Streams, et bien d'autres. Par exemple, la méthode getText() sur une URL vous permet de récupérer le contenu de l’URL distante via une requête HTTP GET. Notez également que, dans vos modules, les méthodes d'extension sont également comprises par le vérificateur statique et le compilateur. Jetons maintenant un œil à la façon dont vous pouvez ajouter de nouvelles méthodes à des types existants.
Contribuer à une méthode d'instance
Pour ajouter de nouvelles méthodes à un type existant, vous devez créer une classe support qui contiendra ces méthodes. À l'intérieur de cette classe support, toutes les méthodes d’extension seront, réellement, public (valeur par défaut en Groovy mais nécessaire pour une implémentation en Java) et static (même si elles seront disponibles sur des instances de cette classe). Elles prendront toujours un premier paramètre qui est en fait l'instance sur laquelle cette méthode sera appelée. Puis, les paramètres suivants seront les paramètres passés lors de l'appel de la méthode. C'est la même convention que celle utilisée pour les catégories Groovy.
Disons que nous voulons ajouter la méthode greets() sur String, qui salue le nom de la personne passé en paramètres, afin que vous puissiez utiliser cette méthode comme suit:
assert "Guillaume".greets("Paul") == "Hi Paul, I'm Guillaume"
Pour réaliser cela, vous allez créer une classe support avec une méthode d’extension de la manière suivante:
package com.acme
class MyExtension {
static String greets(String self, String name) {
"Hi ${name}, I'm ${self}"
}
}
Contribuer à une méthode statique
Pour les méthodes d’extension statique, c’est le même mécanisme et la même convention. Ajoutons une nouvelle méthode static à Random pour obtenir un entier aléatoire compris entre deux valeurs, vous pouvez procéder de la même façon que dans cette classe:
package com.acme
class MyStaticExtension {
static String between(Random selfType, int start, int end) {
new Random().nextInt(end - start + 1) + start
}
}
Ainsi, vous êtes en mesure d’utiliser cette méthode d’extension de la façon suivante:
Random.between(3, 4)
Descripteur de module d'extension
Une fois que vous avez codé vos classes support (en Groovy ou en Java) contenant les méthodes d’extension, vous avez besoin de créer un descripteur pour votre module. Vous devez pour cela créer un fichier nommé org.codehaus.groovy.runtime.ExtensionModule dans le répertoire META-INF/services de l'archive de votre module. Quatre champs utiles peuvent être définis, pour indiquer à l’exécution de Groovy le nom et la version de votre module, ainsi que vos classes support pour vos méthodes d'extension définies sous la forme d'une liste et séparées par des virgules. Voilà à quoi ressemble notre descripteur de module final :
moduleName = MyExtension
moduleVersion = 1.0
extensionClasses = com.acme.MyExtension
staticExtensionClasses = com.acme.MyStaticExtension
Avec ce descripteur de module d'extension dans le classpath, vous êtes maintenant en mesure d’utiliser ces méthodes dans votre code, sans avoir besoin d'un import ou quoi que ce soit d'autre, car elles sont automatiquement enregistrées.
Saisir une extension
Avec l'annotation @Grab dans vos scripts, vous pouvez récupérer les dépendances depuis un dépôt Maven, comme Maven Central. Avec l'ajout de l'annotation @GrabResolver, vous pouvez spécifier votre propre emplacement pour vos dépendances. Si vous "saisissez" une dépendance sur un module d'extension, grâce à ce mécanisme, la méthode d'extension sera également installée automatiquement. Idéalement, pour que ce soit cohérent, le nom de votre module et de votre version doivent être cohérents avec l'id et la version de votre artefact.
Résumé
Groovy est très populaire parmi les développeurs Java et offre une plate-forme mature et un écosystème pour leurs besoins d'application. Mais sans pour autant se reposer, l'équipe de développement Groovy continue d'améliorer encore le langage et son API pour permettre à ses utilisateurs d'accroître leur productivité sur la plate-forme Java.
Groovy 2.0 répond à trois grands thèmes:
- Plus de performances: avec le support de l’instruction Invoke Dynamic du JDK 7 pour accélérer Groovy, pour ceux qui ont la chance d'avoir JDK 7 déjà en production, mais aussi avec la compilation statique pour le JDK 5 et au-delà pour tout le monde. Ceci s'applique, en particulier, pour ceux qui sont prêts à renoncer à certains aspects dynamiques pour se protéger de la portée des "monkey patching" et pour obtenir la même vitesse que Java.
- Plus de convivialité Java: avec le support des améliorations Java 7 de Project Coin gardant Groovy et Java aussi proche syntaxiquement que jamais et avec le vérificateur statique des types permettant d'avoir le même niveau de commentaires et de sûreté du typage que celui fourni par le compilateur javac pour les développeurs qui utilisent Groovy comme un langage de script Java.
- Plus de modularité: avec un nouveau niveau de modularité, Groovy ouvre les portes pour de plus petits livrables. Par exemple, pour une intégration dans des applications mobiles sur Android. Et permets à l'API Groovy de croître et d'évoluer avec des versions et des modules d'extension plus récents, tout en permettant aux utilisateurs de contribuer à des méthodes d'extension pour les types existants.
A propos de l’auteur
À la tête du développement de Groovy pour SpringSource, une division de VMware, Guillaume Laforge est le manager officiel du projet Groovy, en dirigeant le projet du langage dynamique Groovy à Codehaus.
Il a initié la création du framework d’application web Grails et a fondé le projet Gaelyk, une boîte à outils légère pour développer des applications Google App Engine en Groovy. Il est également un conférencier régulier pour les présentations de Groovy, Grails, Gaelyk, Domain-Specific Languages à JavaOne, GR8Conf, SpringOne2GX, QCon et Devoxx, entre autres.
Guillaume est aussi l’un des membres fondateurs du podcast Java/OSS/IT Français LesCastCodeurs.