Kompilieren und Ausführen von Java-Code zur Laufzeit / Sudo Null IT News

Hallo Habr! Heute möchte ich über die dynamische Kompilierung und Ausführung von Java-Code sprechen, ähnlich wie bei Skript-Programmiersprachen. In diesem Artikel finden Sie eine Schritt-für-Schritt-Anleitung zum Kompilieren von Java zu Bytecode und zum spontanen Laden neuer Klassen in den ClassLoader.

Wozu?

In der Entwicklung gibt es immer mehr typische Aufgaben, die durch einfache Codegenerierung erledigt werden könnten. Generieren Sie beispielsweise DTO-Klassen gemäß der bestehenden Spezifikation gemäß den OpenAPI- oder AsyncAPI-Standards. Im Allgemeinen ist es zum Generieren von Code nicht erforderlich, den Code zur Laufzeit zu kompilieren und auszuführen, da Sie Klassenquellen generieren und zusammen mit dem Projekt kompilieren können. Beim Schreiben von Tools für die Codegenerierung wäre es jedoch schön, dies mit Tests abzudecken. Und wenn Sie das offensichtlichste Szenario überprüfen: generiert-kompiliert-heruntergeladen-geprüft-gelöscht. Und hier entsteht die Aufgabe, Code „on the fly“ zu generieren und zu prüfen.

Außerdem ist es manchmal erforderlich, Code aus der Ferne auszuführen. In der Regel handelt es sich dabei um eine Art verteiltes Cloud Computing. In diesem Fall können Sie den Quellcode an den Rechenknoten senden, und dort findet eine dynamische Assemblierung und Ausführung statt.

Sequenzierung

Um Java-Code zur Laufzeit auszuführen, benötigen wir:

  1. Erstellen Sie unseren Code dynamisch und speichern Sie ihn in einer .java-Datei.

  2. Kompilieren Sie Quellen in Bytecode (.class-Dateien).

  3. Kompilierte Klassen in ClassLoader laden.

  4. Verwenden Sie die Reflexions-API, um Methoden abzurufen und auszuführen.

Schritt 1. Codegenerierung

Im Allgemeinen können Sie zum Generieren von Quellcodes natürlich einfach Text über StringBuider in eine Datei schreiben und zufrieden sein. Aber ich möchte mehr angewandte Lösungen zeigen, also betrachten wir die Option, Code mit dem Paket zu generieren com.sun.codemodela genau hier Es gibt ein gutes Tutorial zu diesem Paket. Es gibt auch eine darauf basierende Bibliothek jsonschema2pojo um Code basierend auf jsonschema zu generieren. Also zum Code:

public void generateTestClass() löst JClassAlreadyExistsException, IOException { //erstelle ein Modell, dies ist sozusagen die Wurzel deines Codebaums JCodeModel codeModel = new JCodeModel(); //definiere unsere Habr-Klasse im Hallo-Paket JDefinedClass testClass = codeModel._class(“hello.Habr”); // Definiere die helloHabr-Methode JMethod method = testClass.method(JMod.PUBLIC + JMod.STATIC, codeModel.VOID, “helloHabr”); // gib den String „Hello Habr!“ im Methodenrumpf aus method.body().directStatement(“System.out.println(\”Hallo Habr!\”);”); //Modell erstellen und Pakete in das aktuelle Verzeichnis schreiben codeModel.build(Paths.get(“.”).toAbsolutePath().toFile()); }

Das obige Beispiel generiert eine Habr.java-Klasse mit einer Methode:

Paket hallo; public class Habr { public static void helloHabr() { System.out.println(“Hallo Habr!”); } }

Schritt 2 Kompilieren des Codes

Um in Bytecode zu kompilieren, wird normalerweise javac verwendet und mit einem einfachen Befehl ausgeführt:

javac -sourcepath src -d build\classes hello\Habr.java

Allerdings müssen wir unsere Klasse direkt aus dem Code kompilieren. Und dafür gibt es eine Compiler-Bibliothek, die man erreichen kann javax/tools/JavaCompiler. Dies ist die Umsetzung javax/tools/Tool (was darin liegt /lib/tools.jar). Es wird in etwa so aussehen:

PathsrcPath = Paths.get(“Hallo”); List files = Files.list(srcPath) .map(Path::toFile) .collect(Collectors.toList()); //den Java-Compiler abrufenCompiler-Compiler = ToolProvider.getSystemJavaCompiler(); //eine neue fileManager-Instanz für unseren Compiler abrufen try(StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)){ //eine Liste aller Dateien abrufen, die Quellen beschreiben Iterable javaFiles = fileManager.getJavaFileObjectsFromFiles(files); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); //Kompilierungsaufgabe starten JavaCompiler.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, null, null, javaFiles ); //Aufgabe ausführen task.call(); //Kompilierungsfehler anzeigen für (Diagnosediagnose: diagnostics.getDiagnostics()) { System.out.format(“Fehler in Zeile %d in %s%n”, diagnostic.getLineNumber(), diagnostic.getSource( )); } }

Schritt 3: Laden Sie den Code herunter und führen Sie ihn aus

Um den Code auszuführen, müssen wir ihn über den ClassLoader laden und unsere Methode über die Reflection-API aufrufen.

// wir bekommen den ClassLoader, es ist besser, den Loader von der aktuellen Klasse zu bekommen, // ich habe es von System gemacht, damit das Beispiel funktioniert ClassLoader classLoader = System.class.getClassLoader(); //Ermittle den Pfad zu unserem Ordner mit generiertem Code URLClassLoader urlClassLoader = new URLClassLoader( new URL[]{Pfade.get(“.”).toUri().toURL()}, classLoader); // unsere Klasse Class laden helloHabrClass = urlClassLoader.loadClass(“hello.Habr”); //HelloHabr-Methode finden und aufrufen Method methodHelloHabr = helloHabrClass.getMethod(“helloHabr”); //im Parameter wird eine Referenz auf eine Instanz der Klasse übergeben, um die Methode aufzurufen //oder null beim Aufruf der statischen Methode methodHelloHabr.invoke(null);

Ergebnis

In diesem Artikel habe ich versucht, ein vollständiges Skript zum Generieren und Ausführen von Code in Runtime zu zeigen. Es war praktisch für mich beim Schreiben von Komponententests für die Bibliothek zum Generieren von DTO-Klassen basierend auf der von der Bibliothek generierten Dokumentation Frühlingswolf. Sie können die Implementierung von Tests in meinem Projekt sehen hier.

Similar Posts

Leave a Reply

Your email address will not be published.