Scripting und Compilation JOHANNES KEPLER UNIVERSITY LINZ Research and teaching network Pratikum SWE 2 © M. Löberbauer, T. Kotzmann, H. Prähofer Scripting und Compilation Scripting in der Java VM Compilation Scripting Ab Java 6 Unterstützung von Script-Sprachen Einfaches API für die Ausführung von Scripts innerhalb der Java VM Basissystem für die Implementierung von Script-Engines Installation von Script-Sprachen über Java Services Mozilla Rhino JavaScript Engine bereits installiert package javax.script Vorgehen Erzeugen eines ScriptEngineManagers Abrufen einer Script-Engine einer bestimmten (installierten) ScriptSprachen über ScriptEngineManager Ausführen von Scripts Behandlung von ScriptExceptions ScriptEngineManager factory = new ScriptEngineManager(); ScriptEngine engine = factory.getEngineByName( "JavaScript" ); try { engine.eval( "println('Hello World');" ); } catch (ScriptException e) { System.out.println( "Error in script: line " + e.getLineNumber() + ", colum " + e.getColumnNumber() + ", message " + e.getMessage() ); } oder Ausführen eines Script-Programms aus einem File (Verwendung eines Readers) engine.eval(new java.io.FileReader( "SimpleGreeter.js" )); Verwendung von Variablen und Bindings Binden von Variablen mit put engine.put( "x" , 1); engine.eval( "x = x + 1;" ); engine.put( "frame" , new JFrame()); engine.eval( "frame.setVisible(true);" ); Abrufen von Variablenwerten mit get Object x = engine.get("x"); System.out.println(x); JFrame frame = (JFrame)engine.get( "frame" ); frame.setVisible(false); 2 Verwendung von Bindings Bindings scope = engine.createBindings(); scope.put( "x" , "xxx" ); engine.eval( "x = x + x;" , scope); Object xxx = scope.get( "x" ); System.out.println(xxx); Evaluierung im Scope x = engine.get( "x" ); System.out.println(x); xxxxxx 2 Wert im globalem Scope bleibt erhalten Funktions- und Methodeaufrufe Engines müssen Invocable implementieren Invocalbe inv = (Invocable) engine; Aufruf von Script-Funktionen engine.eval( "function greet(x) {return 'Hello, ' + x + '!'; }" ); String greeting = ((String)inv.invokeFunction( "greet" , "Franz" )); System.out.println(greeting); Hello, Franz! Aufruf von Methoden (bei objekt-orientierten Script-Sprachen) engine.eval(new FileReader( "SimpleGreeter.js" )); Object goodbyeGreeter = engine.eval( "new SimpleGreeter('Goodbye')" ); "SimpleGreeter.js" function SimpleGreeter(salutation) {this.salutation = salutation; } SimpleGreeter.prototype.greet = function(whom) { return this.salutation + ", " + whom + "!" } System.out.println(inv.invokeMethod(goodbyeGreeter, "greet", "Ann" )); Goodbye, Ann! Implementierung von Interfaces Java-Interfaces können durch Script-Objekte implementiert werden Mit getInterface(Object, Class<T>) von Invocable engine.eval( "var obj = new Object(); obj.run = function() { println('run method called'); }" ); Object obj = engine.get( "obj" ); Invocalbe inv = (Invocable) engine; Runnable r = inv.getInterface(obj, Runnable.class); Thread thread = new Thread(r); thread.start(); run method called Compilation von Scripts Scripts können compiliert werden mit compile von Compilable Engine muss Compilable implementieren erzeugt CompiledScript das mit eval ausgeführt werden kann Compilable compEngine = (Compilable)engine; CompiledScript script = compEngine.compile( "println ('compiled script called')" ); script.eval(); compiled script called Beispiel Interaktive Java-Script Anwendung public class JSCalculator { private JTextArea scriptArea; private ScriptEngine jsEngine; privare execBtn; ... public JSCalculator() { ScriptEngineManager manager = new ScriptEngineManager(); jsEngine = manager.getEngineByExtension("js"); frame = new JFrame("JS Calculator"); execBtn = new Jbutton("Exec"); execBtn.addActionListener(execHandler); ... } TextArea mitJavaScript Code private ActionListener execHandler = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String script = scriptArea.getText(); Object result; try { result = jsEngine.eval(script); scriptListModel.addElement(script); … scriptArea.setText(" "); } catch (ScriptException e) { resultLabel.setText("Error at column " + e.getColumnNumber() + ": " + e.getMessage()); } catch (Exception e) { resultLabel.setText("Exception occurred: " + e.getMessage()); } Installation von Script-Sprachen Script-Engine werden als Jar-Libraries installiert Müssen java.script.ScriptEngineFactory Service implementieren Registrierung als Service in META-INF Auffinden ScriptEngineManager manager = new ScriptEngineManager(); System.out.println( "Available factories: " ); for (ScriptEngineFactory factory : manager.getEngineFactories()) { System.out.println(factory.getEngineName()); } Service Discovery! final ScriptEngine engine = manager.getEngineByName( "groovy" ); Object result = engine.eval( "x = 1;" ); Exkurs Java Services Java Service und Service Loader Implementierungen zur Laufzeit anfordern Java Service Interface = Spezifikation des Service Java Service Provider = Implementierung des Service ServiceLoader = Mechanismus zu Auffinden und Laden der Service Provider zur Laufzeit Vorgehen: Service Interface definieren Service Provider implementieren registieren: in META-INF/services/<full-qualified-servicename> in Jar-Datei verpacken Mit ServiceLoader Provider laden Beispiel Java Services Service Interface definieren package services; public interface MyService { public void doService(); } Service Provider implementieren package services; public class FirstProvider implements MyService { @Override public void doService() { System.out.println("First Service"); } package services; } public class SecondProvider implements MyService { @Override public void doService() { System.out.println(“Second Service"); } } registieren: in META-INF/services/<full-qualified-servicename> services.FirstProvider services.SecondProvider Beispiel Java Services Mit ServiceLoader Provider laden package services; import java.util.ServiceLoader; public class TestMyServices { public static void main(String[] args) { ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class); for (MyService provider: loader) { provider.doService(); } } } Scripting und Compilation Scripting in der Java VM Compilation Compiler Tools API Zugriff auf Javac-Compiler über Tools API JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); Ausführen des Java-Compilers durch run 0 für Erfolg Streams für Ein-/Ausgabe (Meldungen etc.) Argumente wie bei Aufruf von javac int run(InputStream in, OutputStream out, OutputStream err, String... arguments); Standard Ein-/Ausgabe int r = compiler.run(null, null, null, "-sourcepath", ".", "SayHalloClass.java"); CompilationTasks CompliationTask erlaubt genaue Kontrolle über Compilation-Prozess fileManager: Manager für Source- und Class-Files diagnosticListeners: Behandlung von Fehlermeldungen classes: Klassen für Annotations-Verarbeitung (hier nicht verwendet) compilationUnits: Source-Code CompilationTask getTask(Writer out, JavaFileManager fileManager, DiagnosticListener<? super JavaFileObject> diagnosticListener, Iterable<String> options, Iterable<String> classes, Iterable<? extends JavaFileObject> compilationUnits); Beispiel: Stamdard: Lesen und Schreiben auf Filesystem StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>(); Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(Arrays.asList( "SayHalloClass.java" )); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits); boolean success = task.call(); Bsp: Generierung und Compilieren von generiertem Code Basisklasse mit abstrakter Methode addEventHandlers public abstract class ButtonFrame extends JFrame { public ButtonFrame() { panel = new JPanel(); yellowButton = new JButton("Yellow"); blueButton = new JButton("Blue"); redButton = new JButton("Red"); ... addEventHandlers(); } protected abstract void addEventHandlers(); … } Textfile action.properties mit Action-Code für Buttons yellowButton=panel.setBackground(java.awt.Color.YELLOW); blueButton=panel.setBackground(java.awt.Color.BLUE); redButton=panel.setBackground(java.awt.Color.RED); Genierierter Code package x; public class Frame extends com.horstmann.corejava.ButtonFrame { protected void addEventHandlers() { yellowButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent event) { panel.setBackground(java.awt.Color.YELLOW); } } ); redButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent event) { panel.setBackground(java.awt.Color.RED); } } ); blueButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent event) { panel.setBackground(java.awt.Color.BLUE); } } ); } } Bsp: Generierung und Compilieren von generiertem Code 1. Source-Code wird mit StringBuilder dynamisch erzeugt 2. Byte-Code wird in Byte-Array abgelegt 3. Code wird aus Byte-Array geladen und ausgeführt Ad 1.) JavaFileObject mit StringBuilder als Quelle public class StringBuilderJavaSource extends SimpleJavaFileObject { public StringBuilderJavaSource(String name) { super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); code = new StringBuilder(); } Name des „FileObjects“ public CharSequence getCharContent(boolean ignoreEncodingErrors) { return code; } public void append(String str) { code.append(str); code.append('\n'); } private StringBuilder code; } liefert Source-Code für Erzeugung des Source-Codes Beispiel: Generierung und Compilieren von Code Ad 1.) Erzeugen des Source-Codes aus Template static JavaFileObject buildSource(String superclassName) throws IOException { StringBuilderJavaSource source = new StringBuilderJavaSource("x.Frame"); source.append("package x;\n"); source.append("public class Frame extends " + superclassName + " {"); source.append("protected void addEventHandlers() {"); Properties props = new Properties(); props.load(new FileReader("action.properties")); for (Map.Entry<Object, Object> e : props.entrySet()) { String beanName = (String) e.getKey(); String eventCode = (String) e.getValue(); source.append(" " + beanName + ".addActionListener(new java.awt.event.ActionListener() {"); source.append(" public void actionPerformed(java.awt.event.ActionEvent event) {"); source.append(" " + eventCode); source.append(" } } );"); } source.append("} }"); return source; } } Event Code aus Datei action.properties wird in actionPerformed von ActionsListener verpackt und bei Komponenten angefügt! Beispiel: Generierung und Compilieren von Code Ad 2.) JavaFileObject mit ByteArrayOutputStream für Byte-Code public class ByteArrayJavaClass extends SimpleJavaFileObject { public ByteArrayJavaClass(String name) { super(URI.create("bytes:///" + name), Kind.CLASS); stream = new ByteArrayOutputStream(); } public OutputStream openOutputStream() throws IOException { return stream; } Name des „FileObjects“ OutputStream für Compiler public byte[] getBytes() { return stream.toByteArray(); } private ByteArrayOutputStream stream; } liefert Byte-Code Beispiel: Generierung und Compilieren von Code Ad 2.) Erzeugen eines FileManagers, der entsprechende FileObjects liefert ForwardingJavaFileManager Delegation aller nichtüberschriebenen Aufrufe zu diesem FileManager JavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); fileManager = new ForwardingJavaFileManager<JavaFileManager>(fileManager) { public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) throws IOException { if (className.startsWith("x.")) { ByteArrayJavaClass fileObject = new ByteArrayJavaClass(className); classFileObjects.add(fileObject); return fileObject; } else { return super.getJavaFileForOutput(location, className, kind, sibling); } } }; Liefert ByteArrayJavaClass für alle Klassen im Package "x" Beispiel: Generierung und Compilieren von Code Ad 3.) Compilation JavaFileObject source = buildSource("com.horstmann.corejava.ButtonFrame"); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, Arrays.asList(source)); Boolean result = task.call(); Laden Map<String, byte[]> byteCodeMap = new HashMap<String, byte[]>(); for (ByteArrayJavaClass cl : classFileObjects) { byteCodeMap.put(cl.getName().substring(1), cl.getBytes()); } ClassLoader loader = new MapClassLoader(byteCodeMap); Class<?> cl = loader.loadClass("x.Frame"); public class MapClassLoader extends ClassLoader { public MapClassLoader(Map<String, byte[]> classes) { this.classes = classes; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classBytes = classes.get(name); if (classBytes == null) throw new ClassNotFoundException(name); Class<?> cl = defineClass(name, classBytes, 0, classBytes.length); if (cl == null) throw new ClassNotFoundException(name); return cl; } Ausführen (über Reflection) JFrame frame = (JFrame) cl.newInstance(); frame.setTitle("CompilerTest"); frame.setVisible(true); private Map<String, byte[]> classes; } Zusammenfassung Scripting API erlaubt Verwendung von Script-Sprachen in JavaAnwendungen Compiler Tool API erlaubt Verwendung des Java-Compilers in JavaAnwendungen Java Service API wird zum Auffinden von installierten Script-Sprachen und Compilern verwendet