Niveau : | Supérieur |
Le scripting est utilisé depuis longtemps, dans un premier temps, pour automatiser certaines tâches sur des systèmes d'exploitation (exemple le shell sous Linux) puis sous la forme de langages de développement (exemple Perl, Python, ...)
Ces langages n'ont pas pour but de remplacer le langage Java mais ils peuvent avoir une place de choix pour remplir certaines tâches et permettre de bénéficier des points forts de Java et du scripting.
Les langages de scripting possèdent plusieurs caractéristiques qui peuvent être intéressantes :
La plate-forme Java permet depuis longtemps d'utiliser des langages de scripts notamment avec des solutions open source comme BeanShell.
Java 6 intègre une API standard, indépendante du langage de scripting utilisé du moment qu'il est compatible avec l'API.
Certains langages de scripting ont été spécifiquement développés pour la JVM : c'est notamment le cas de Groovy.
Ce chapitre contient une section :
Java SE 6.0 intègre la possibilité d'utiliser des moteurs de scripting suite à l'intégration des spécifications de la JSR 223.
La JSR 223 a pour but d'intégrer des possibilités de scripting dans les applications Java en permettant :
Les classes et interfaces de cette fonctionnalité sont regroupées dans le package javax.script.
L'API propose un support pour tous les moteurs de scripting compatibles avec elle.
Java SE 6.0 intègre en standard le moteur de scripting Rhino version 1.6 R2 qui propose un support pour le langage Javascript.
La gestion des moteurs utilisables se fait grâce à la classe ScriptEngineManager : elle permet d'obtenir la liste des objets de type ScriptEngineFactory de chaque moteur de scripting installé. Ces méthodes ne sont pas statiques, il est donc nécessaire d'instancier un objet de type ScriptEngineManager pour les utiliser.
Des fabriques permettent l'instanciation d'un objet de type ScriptEngine qui encapsule le moteur de scripting.
Exemple : |
package com.jmd.tests.java6;
import java.util.List;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
public class ListerScriptEngine {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory : factories) {
System.out.println("Name : " + factory.getEngineName());
System.out.println("Version : " + factory.getEngineVersion());
System.out.println("Language name : " + factory.getLanguageName());
System.out.println("Language version : " + factory.getLanguageVersion());
System.out.println("Extensions : " + factory.getExtensions());
System.out.println("Mime types : " + factory.getMimeTypes());
System.out.println("Names : " + factory.getNames());
}
}
}
Résultat : |
Name : Mozilla Rhino
Version : 1.6 release 2
Language name : ECMAScript
Language version : 1.6
Extensions : [js]
Mime types:[application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Names : [js, rhino, JavaScript, javascript, ECMAScript, ecmascript]
Les propriétés Extensions, MimeType et Names sont importantes car elles sont utilisées pour obtenir une instance de la classe ScriptEngine.
Le ScriptEngineManager permet d'obtenir directement une instance du moteur de scripting à partir d'un nom, d'une extension et d'un type mime particulier respectivement grâce aux méthodes getEngineByName(), getEngineByExtension() et getEngineByMimeType().
Exemple : |
package com.jmd.tests.java6;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class TestScriptEngine {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur1 = manager.getEngineByName("rhino");
ScriptEngine moteur2 = manager.getEngineByExtension("js");
ScriptEngine moteur3 = manager.getEngineByName("test");
if (moteur3== null) {
System.out.println("Impossible de trouver le moteur test ");
}
}
}
Si aucune fabrique ne correspond au paramètre fourni alors l'instance de type ScriptEngine retournée est null.
Il est possible d'ajouter d'autres moteurs de scripting. Le projet scripting hébergé par java.net propose l'encapsulation de nombreux moteurs de scripting pour l'utilisation avec l'API Scripting. https://scripting.dev.java.net/
Il faut télécharger le fichier jsr223-engines.zip. Cette archive contient un répertoire pour chaque moteur. Il faut ajouter le fichier build/xxx-engine.jar au classpath où xxx est le nom du moteur.
Résultat de l'exécution : |
Name : Mozilla Rhino
Version : 1.6 release 2
Language name : ECMAScript
Language version : 1.6
Extensions : [js]
Mime types:[application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Names : [js, rhino, JavaScript, javascript, ECMAScript, ecmascript]
Name : jython
Version : 2.1
Language name : python
Language version : 2.1
Extensions : [jy, py]
Mime types : []
Names : [jython, python]
Il est nécessaire pour instancier le moteur que celui-ci soit présent dans le classpath. Dans le cas de jython, il faut ajouter le fichier jython.jar dans le classpath. Pour cela, il faut télécharger le fichier jython_Release_2_2alpha1.jar et l'exécuter en double cliquant dessus. Le programme d'installation utilise un assistant :
Ajoutez le fichier jython.jar contenu dans le répertoire d'installation au classpath de l'application. Il est alors possible de créer une instance du moteur de script Jython.
Exemple : |
package com.jmd.tests.java6;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class TestJython {
public static void main(String args[]) {
try {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("jython");
if (moteur== null) {
System.out.println("Impossible de trouver le moteur jython ");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Si le fichier jython.jar n'est pas présent dans le classpath une exception est levée.
Exemple : |
Exception in thread "main" java.lang.NoClassDefFoundError: org/python/core/PyObject
at com.sun.script.jython.JythonScriptEngineFactory.getScriptEngine(
JythonScriptEngineFactory.java:132)
at javax.script.ScriptEngineManager.getEngineByName(ScriptEngineManager.java:225)
at com.jmd.tests.java6.TestJython.main(TestJython.java:11)
La classe ScriptEngine propose plusieurs surcharges de la méthode eval() pour exécuter un script. Ces surcharges attendent en paramètre le script sous la forme d'une chaîne de caractères ou d'un flux de type Reader.
Exemple : |
package com.jmd.tests.java6;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class TestJython {
public static void main(String args[]) {
try {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("jython");
if (moteur == null) {
System.out.println("Impossible de trouver le moteur jython ");
} else {
moteur.eval("print \"test\"");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
La méthode eval() peut lever une exception de type javax.script.ScriptExceptionsi le moteur détecte une erreur dans le script.
Exemple : |
package com.jmd.tests.java6;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestRhino {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("rhino");
try {
moteur.eval("alert('test');");
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Résultat : |
javax.script.ScriptException: sun.org.mozilla.javascript.internal.EcmaError: ReferenceError:
"Alert" n'est pas défini (<Unknown source>#1) in <Unknown source> at line number 1
at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.java:110)
at com.sun.script.javascript.RhinoScriptEngine.eval(RhinoScriptEngine.java:124)
at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:247)
at com.jmd.tests.java6.TestRhino.main(TestRhino.java:13)
Deux surcharges de la méthode eval() attendent en paramètre un objet de type Bindings. C'est une paire clé/valeur qui permet de passer des objets Java au script.
Exemple : |
package com.jmd.tests.java6;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestBindings {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("rhino");
try {
Bindings bindings = moteur.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.clear();
bindings.put("entree","valeur");
moteur.eval("var sortie = '';"+
" sortie = entree + ' modifiee '", bindings);
String resultat = (String)bindings.get("sortie");
System.out.println("resultat = "+resultat);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Résultat : |
resultat = valeur modifiee
La classe ScriptEngine possède deux méthodes pour faciliter l'utilisation des Bindings : les méthodes put() et get() pour respectivement passer un objet au script et obtenir un objet du script.
Exemple : |
package com.jmd.tests.java6;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestBindings2 {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("rhino");
try {
moteur.put("entree","valeur");
moteur.eval("var sortie = '';"+
" sortie = entree + ' modifiee '");
String resultat = (String)moteur.get("sortie");
System.out.println("resultat = "+resultat);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Deux surcharges de la méthode eval() attendent en paramètre un objet de type ScriptContext qui permet de préciser la portée des Bindings.
Il existe deux portées prédéfinies :
Il est possible de préciser le contexte par défaut du moteur en utilisant la méthode SetContext() de la classe ScriptEngine.
La méthode getBindings() permet d'obtenir les bindings pour la portée fournie en paramètre.
Les scripts sont généralement interprétés : ils doivent donc être lus, validés et évalués avant d'être exécutés. Ces opérations peuvent être coûteuses en ressources et en temps.
L'interface Compilable propose de compiler ces scripts afin de rendre leurs exécutions plus rapides. L'implémentation de cette interface par un moteur de scripting est optionnelle : il faut donc vérifier que l'instance du moteur de scripting implémente cette interface et la caster vers le type Compilable avant d'utiliser cette fonctionnalité.
La méthode compile() réalise une compilation du script et retourne un objet de type CompiledScript en cas de succès.
Le script compilé est exécuté avec la méthode eval() de la classe CompiledScript.
Exemple : |
package com.jmd.tests.java6;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestCompilable {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("rhino");
try {
Bindings bindings = moteur.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.clear();
bindings.put("compteur", 1);
if (moteur instanceof Compilable) {
Compilable moteurCompilable = (Compilable) moteur;
CompiledScript scriptCompile = moteurCompilable
.compile("var sortie = '';"
+ "sortie = 'chaine' + compteur;"
+ "compteur++;");
for (int i = 1; i < 11; i++) {
scriptCompile.eval(bindings);
String resultat = (String) bindings.get("sortie");
System.out.println("valeur " + i + " = " + resultat);
}
} else {
System.err
.println("Le moteur n'implemente pas l'interface Compilable");
}
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Résultat : |
valeur 1 = chaine1
valeur 2 = chaine2
valeur 3 = chaine3
valeur 4 = chaine4
valeur 5 = chaine5
valeur 6 = chaine6
valeur 7 = chaine7
valeur 8 = chaine8
valeur 9 = chaine9
valeur 10 = chaine10
L'utilisation de cette fonctionnalité est particulièrement intéressante pour des exécutions répétées du script.
Cette interface permet d'invoquer une fonction définie dans le code source du script.
Dès qu'une fonction a été évaluée par le moteur de scripting, elle peut être invoquée grâce à la méthode invoke() de l'interface Invocable. L'implémentation de cette interface par un moteur de scripting est optionnelle : il faut donc vérifier avant d'utiliser cette fonctionnalité si le moteur implémente cette interface.
Il est possible de fournir des paramètres à la fonction invoquée.
Exemple : |
package com.jmd.tests.java6;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestInvocable {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("rhino");
try {
moteur.eval("function afficher(valeur) {"
+ "var sortie = '';"
+ "sortie = 'chaine' + valeur;"
+ "return sortie;"
+ "}");
if (moteur instanceof Invocable) {
Invocable moteurInvocable = (Invocable) moteur;
Object resultat = moteurInvocable.invokeFunction("afficher",
new Integer(10));
System.out.println("resultat = " + resultat);
} else {
System.err.println("Le moteur n'implemente pas l'interface Invocable");
}
} catch (ScriptException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
Résultat : |
resultat = chaine10
La méthode getInterface() de l'interface Invocable permet d'obtenir dynamiquement un objet dont la ou les méthodes sont codées dans le script.
L'exemple ci-dessous va définir une fonction run() qui sera invoquée dans un thread en utilisant la méthode getInterface() avec en paramètre un objet de type Class qui encapsule la classe Runnable
Exemple : |
package com.jmd.tests.java6;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class TestInvocable2 {
public static void main(String args[]) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine moteur = manager.getEngineByName("rhino");
try {
moteur.eval("function run(){"
+ "for (i = 0 ; i < 1000 ; i ++) {"
+ "print('run'+i);"
+ "}"
+ "}");
if (moteur instanceof Invocable) {
Invocable moteurInvocable = (Invocable) moteur;
Runnable runnable = moteurInvocable.getInterface(Runnable.class);
Thread thread = new Thread(runnable);
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
}
thread.join();
} else {
System.err.println("Le moteur n'implemente pas l'interface Invocable");
}
} catch (ScriptException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Extrait du résultat : |
main988
run174run175main989
main990
main991
main992
main993
main994
main995
main996
main997
main998
main999
run176run177run178run179run180
L'itération du programme s'exécute beaucoup plus rapidement que le thread : l'appel à la méthode join() du thread permet d'attendre la fin de son exécution avant de terminer l'application.
La commande jrunscript est un outil du JDK utilisable en ligne de commande qui permet d'exécuter des scripts.
Remarque : pour pouvoir utiliser cette commande, il est nécessaire d'ajouter dans le path le chemin du répertoire bin du JDK.
Les options -help et -? permettent d'obtenir la liste des options de la commande.
L'option -q permet de connaître la liste des moteurs de scripting utilisables.
Les options -cp et -classpath permettent de préciser le classpath qui sera utilisé.
Si seul le moteur de scripting par défaut est installé alors il n'est pas utile de préciser le moteur à utiliser. Dans le cas contraire, il est nécessaire de préciser le moteur à utiliser grâce à l'option -l
Exemple : |
C:\>jrunscript -q
Language ECMAScript 1.6 implemention "Mozilla Rhino" 1.6 release 2
Sans autre argument, la commande jrunscript affiche un prompt qui permet de saisir le script à évaluer.
Exemple : |
C:\>jrunscript
js> var i = 10* 10;
js> i;
100.0
js> i
100.0
js> i++;
100.0
js> i
101.0
js> print i;
script error: sun.org.mozilla.javascript.internal.EvaluatorException: il manque
';' avant une instruction (<STDIN>#1) in <STDIN> at line number 1
js>