Effective Java Programm schreibt Programm „Code“ -Generierung in Java Michael Hunger Wir alle sind es gewohnt, Java als relativ statische Sprache zu betrachten. Wir schreiben Code, dieser wird compiliert und steht dann über den ClassLoader zur Ausführung zur Verfügung. In anderen Sprachen ist es üblich, ein Programm auch nach dem initialen Laden noch zu verändern, man denke nur an JavaScript oder die Metaprogrammierung von Ruby und Groovy. Wir können das im begrenzten Rahmen auch, mittels Reflection, Virtual Proxies oder den neuen MethodHandle-APIs. Einige Ansätze gehen sogar noch viel weiter, wie JRebel immer wieder beeindruckend beweist. Warum denn eigentlich? Für diverse Anforderungen ist es schon praktisch, ausführbare Java-Klassen zur Lauf- oder Ladezeit zu erzeugen oder zu modifizieren. Insbesondere wenn dynamisches oder nutzergeneriertes Verhalten nicht nur interpretiert, sondern effizient (inkl. JIT) von der JVM ausgeführt werden soll. Man denke nur an dynamische Ausdrücke (siehe SpEL-Artikel von Thomas Darimont [Dari15]) und Datenbankabfragen, die zur Laufzeit compiliert werden sollen. Oder die flexiblen Proxies von Sven Ruppert und Heinz Kabutz [KabRup15a/b]. Ein weiteres Beispiel sind Regeln von Rule- oder BPM-Engines oder andere externe DSLs. Weitere Anwendungsfälle sind die Generierung von Metadaten-Repräsentationen als Klassen, zum Beispiel für Protokollformate, oder Platzhalter für Datenbank-Schemata (Criteria API). Spannend ist auch die Generierung von internen DSLs mit fluent Interfaces und Methoden, zum Beispiel aus Grammatiken oder von Parsern wie zum Beispiel bei ANTLR. E Quellcodegenerierung Wer bei der Umsetzung einer solchen Anforderung einmal mit Code-Erzeugung mittels des Verknüpfens von Strings angefangen hat, ist schnell auf die häufige Duplikation von ähnlichen Aufrufen und Fragmenten gestoßen und hat sich über die fehlende Typsicherheit und Nutzung von Literalen geärgert. Oft führt das zum kurzfristigen Refactoring in einer internen DSL, die dann angenehmer zu nutzen, aber natürlich nicht direkt zielführend für die eigentliche Aufgabe ist. Daher ist es sinnvoll zu wissen, welche nützlichen Tools es in diesem Bereich schon gibt. Bytecode-Generierung Für manche Anwendungsfälle, besonders aber in Bibliotheken oder Java-Agenten, wo man schnell zur Lauf- oder Ladezeit Integrationscode generieren muss, bietet sich eher die BytecodeGenerierung an. Genauso bei Anreicherung von existierenden Systemen zur Laufzeit zum Beispiel mit den klassischen Querschnittsfunktionalitäten wie Auditing oder Sicherheitschecks mittels AspectJ ist Bytecode unabdingbar. Die JVM ist eine Stack-Maschine. Ihr Bytecode ist nicht sonderlich schwer zu lesen, man muss nur gut aufpassen, was auf den Stack bewegt wird und was wieder herunterkommt. Er ist langatmiger als Assembler besonders wegen der Typ- und Methodenliterale. Zum Glück muss man keinen reinen Bytecode manuell eingeben. Viele der Bytecode-Generatoren machen es dem Nutzer aber viel einfacher, da sie diesen entweder über eine DSL oder über das Umwandeln von Code-Fragmenten erzeugen. Einen anderen, sehr beeindruckenden Weg mit extrem effizientem Ergebnis hatte ich vor einer Weile mit dem Gespann aus dem Truffle-AST-Framework und dem Graal-Compiler [Hung14] vorgestellt, das uns hoffentlich in Java 9 dann offiziell beehren wird. Aus all diesen Gründen möchte ich heute einmal verschiedene Ansätze zur Codegenerierung für Java vorstellen. Toolübersicht Die Liste der verfügbaren Tools in Tabelle 1 ist erstaunlich lang und bei Weitem nicht vollständig. Auch wenn es keine alltägliche Aufgabe ist, scheint der Schuh doch oft genug gedrückt zu haben. Verschiedene Autoren haben eine Menge Bibliotheken mit unterschiedlichen Ansätzen veröffentlicht, um den diversen Anforderungen gerecht zu werden. Mit einem Generierungsansatz kann man den Aufwand zur Pflege eines solchen Codes, der aus einer wohldefinierten Quelle reproduziert werden kann, deutlich reduzieren. Auch wird durch einen solchen „SingleSource“-Ansatz das etwaige AuseinanderlauName Art der Generierung fen von manuell gepflegtem Code vermieden. ASM Bytecode Auch wenn das endgültige Ziel JavaCGLib Bytecode Bytecode darstellt, führt der „Umweg“ ByteBuddy Bytecode über Quellcode genauso zum Ziel und Janino Bytecode,Compiler kann teilweise sogar verständlicher sein. Seit Java 6 ist in der tools.jar des JDK ein AspectJ Bytecode, Weaving Laufzeitcompiler [JavaCompiler] enthalten, JavaAssist Bytecode der genutzt werden kann, um Quellcode JavaPoet Quellcode in Bytecode zu transformieren. Dieser Groovy Metaprogrammierung kann dann direkt über ClassLoader oder mittels Instrumentierung geladen werden. javax.tools.JavaCompiler Bytecode Sowohl die JavaDoc des Compilers als Reflection Laufzeitinteraktion auch Heinz Kabutz in seinem Newsletter XText Quellcode 180 [Kabutz10a] beschreiben im Detail, wie dieser genutzt wird. Tabelle 1: Ansätze zur Codegenerierung für Java www.javaspektrum.de aktiv? ja nein ja kaum ja ja ja ja ja ja ja beschrieben ja ja ja nein minimal ja ja nein nein nein nein 55 Effective Java AspectJ AspectJ [AspectJ] wird schon seit 2001 eingesetzt, um Java-Systeme nachträglich durch dynamische und Querschnittsfunktionalitäten anzureichern. Es kann zur Compile- oder Ladezeit generierten Bytecode in existierende Klassen hineinweben und damit alle Aspekte der Ausführung beeinflussen. Von Feldzugriff über Instanziierung bis zur Methodenausführung. In Aspekten werden die AspektJ-Bestandteile definiert. Man kann mit Erweiterungsmethoden neue Funktionalität definieren. Über sogenannte Pointcuts werden die Stellen (Join Points) festgelegt, an denen Funktionalität angebunden werden soll. Und Advices legen fest, welche Funktionalität an einem Pointcut auf welche Art und Weise aktiv wird (davor, danach, stattdessen). AspectJ hatte lange Jahre eine sehr starke Anwendung und Verbreitung (u. a. im Spring-Framework), es ist jetzt aber eher in den Hintergrund getreten. ASM ASM [ASMIntro] ist eines der ältesten (seit 2000) und bewährtesten Tools zur Bytecode-Manipulation. Wegen seiner Kompaktheit und Geschwindigkeit wird es innerhalb vieler anderer Frameworks eingesetzt. Es liegt mittlerweile in Version 5 vor. Mit ASM kann direkt Bytecode erzeugt werden und es können auch Transformationen existierender Klassen und Methoden vorgenommen werden. Diese basieren auf einem extrem schnellen Bytecode-Scanner, der ereignisgesteuert über ein Visitor-API Informationen an den Aufrufer zurückliefert, aber auch erlaubt, währenddessen den Bytecode zu modifizieren. Dabei werden gängige Transformationen und Analysen schon mitgeliefert. Ausgehend vom Classreader wird im ClassVisitor für jeden Aspekt der Klasse (Felder, Konstruktor, Methoden, Annotationen usw.) eine Callback-Methode aufgerufen, wie visitField oder visitMethod. Einige dieser Methoden geben wieder eigene Visitoren (wie MethodVisitor, InstructionAdapter) zurück, die dann weiterhin aufgerufen werden, um tiefere Details zu instrumentieren. ASM bringt in neueren Versionen das Tool ASMifier mit, das für eine gegebene Klasse die notwendigen ASM-Aufrufe generiert, um eine ebensolche Klasse zu erzeugen. Es wird mittels java -cp asm-all-<version>.jar org.objectweb.asm.util.ASMifier java.lang.Integer > IntegerVisitor.java aufgerufen. Prinzipiell läuft die Erzeugung von Bytecode mit ASM so ab: HErzeugung eines ClassWriter. HAufruf von cw.visit() und Übergabe von FQN, Superklassen, Interfaces, Modifiern usw. HFür jedes Feld: Aufruf von cw.visitField(modifier, name, typ, …) und Zuweisung an einen FieldVisitor. HAufruf von fv.visit*() zur Übergabe des Initialisierungscodes. HFür jede Methode: Aufruf von cw.visitMethod(modifier, name, type descriptor, signatur, exceptions) und Zuweisung an einen MethodVisitor. HAufruf von mv.visit*() zur Übergabe von Instruktionen für den Methodenrumpf. HAufruf von cw.visitEnd(). HAufruf von cw.toByteArray() zum Erhalten des Bytecodes. Wenn Klassen nur partiell geändert werden sollen, dann erfolgt das über angepasste Instanzen der Visitoren die in den entsprechenden visit*-Methoden vor, nach oder statt der Su56 perklassen-Aufrufe die entsprechenden Bytecode-Instrumentierungs-Aufrufe durchführen: // Ausgabe des Methodennamens in jeder Methode ... extends MethodVisitor { @Override public void visitCode() { super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); super.visitLdcInsn("method: "+methodName); super.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"); super.visitCode(); } } Javaassist Javassist [JavaassistTutorial], jetzt verfügbar in Version 3.20, macht die Generierung von Bytecode einfach, da er auch JavaQuellcode-Fragmente verarbeitet, die man den API-Methoden als Strings übergibt, welche dann direkt umgewandelt werden. Der generierte Bytecode kann dann an spezifischen Stellen innerhalb von Methoden oder Klassen eingefügt werden. Es gibt auch ein reines Bytecode-API, das die direkte Manipulation erlaubt. Klassen können zur Laufzeit geändert, aber auch beim Laden durch die JVM modifiziert werden. Das ganze basiert auf einer objektorientierten Repräsentation von Klassen (CtClass), Methoden (CtMethod) und Feldern (CtField), die direkt inspiziert, manipuliert und erzeugt werden können. Es gibt einige Einschränkungen, beispielsweise können keine Methoden gelöscht (sondern nur umbenannt) und auch nicht um Parameter ergänzt werden (stattdessen Overloading und Delegation). Neuer Code kann in Methoden am Anfang, am Ende, an bestimmten Zeilen und als umschließender try-catch-Block eingefügt werden. Der Rumpf der Methode kann auch komplett ersetzt werden. Im übergebenen Quellcode können Substitutionen wie $0, $1 für Parameter oder $type für den Ergebnistyp oder $_ für das bisherige Ergebnis genutzt werden. Der Zugriff auf Klassendefinitionen (CtClass) erfolgt über einen ClassPool, der sich auch um weitere Aspekte wie Klassenpfade kümmert. Das Ergebnis der Manipulation kann dann auf diverse Weise in Bytecode beziehungsweise geladene Klassen überführt werden: ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("company.Person"); // ab hier kann die Klasse modifiziert werden cc.setSuperclass(pool.get("company.Entity")); CtMethod m = cc.getDeclaredMethod("call"); m.insertBefore( "{ System.out.println(\"Are you sure you want to call\"+$0+\"?\"); }"); cc.writeFile(); byte[] bytes = cc.toBytecode(); // direkt Klasse erzeugen Class clazz = cc.toClass(); Zugriff auf die darunterliegenden Bytecode-Informationen kann über ctClass.getClassFile() und ctMethod.getMethodInfo() erlangt werden. Per se können Klassen nur modifiziert werden, wenn sie noch nicht geladen wurden. Also entweder vorher oder während des Ladens mit einem ClassTransformer [Müller14]. Mit einem Java-Agenten mit dem Instrumentation-API und redefineClasses könnte man das Neuladen einer Klasse erzwingen, JavaSPEKTRUM 6/2015 Effective Java ebenso mit dem Debugger-API, oder mit der Neuerzeugung des ClassLoaders, der die Klasse bisher geladen hat. Aber natürlich nur, wenn sie den Regeln des JVM-Hot-Reloads entspricht. Das alles wird in [Müller14] gut erklärt. Zurzeit unterstützt der Javaassist-Compiler keine Enums und Generics, ebenso wenig innere (anonyme) Klassen. Zugriff darauf ist nur über die darunterliegenden Bytecode-APIs möglich. .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)",System.class,"Hello, JavaPoet!") .build(); // "HelloWorld" Klasse mit der "main" Methode TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); ByteBuddy // .java Datei mit import Deklarationen und Package ByteBuddy ist eine moderne, kleine, aber schnelle Integrationsbibliothek von Rafael Winterhalter [Winterhalter14b], die darauf spezialisiert ist, existierenden Code miteinander zu verbinden. Sie benutzt ASM unter der Haube, um Bytecode zu manipulieren. Um die Komplexität des Erzeugens von Bytecode zu vermindern, wird zumeist an in statischen Methoden vorliegende Implementierungen delegiert. ByteBuddy benutzt eine kompakte DSL, um die Verknüpfung von Klassendefinition, Ziel und die neuen Aufrufe zu beschreiben. Dabei werden oft Annotationen genutzt, um Ziele der Anpassung zu markieren. Insofern ähnelt es etwas AspectJ, nur dass hier eine interne Java-DSL zum Einsatz kommt. Hier als Beispiel die Implementierung einer einfachen Absicherung von annotierten Methoden für eine notwendige Rolle: class ByteBuddySecurityLibrary implements SecurityLibrary { // "Speicher" für aktuellen User public static User currentUser = User.anonymous; @Override public Class<? extends T> secure(Class type) { return new ByteBuddy() // mit @Secured annotierte Methoden .method(isAnnotatedBy(Secured.class)) // Delegation an diese ByteBuddySecurityLibrary.intercept .intercept(MethodDelegation.to(ByteBuddySecurityLibrary.class)) .make() .load(type.getClassLoader(), ClassLoadingStrategy.Default.INJECTION) .getLoaded(); } } JavaPoet JavaPoet von Square bietet eine interne DSL zum Generieren von Java-Code. Sie nutzt eine Builder-DSL mit fluent Interfaces, um Methoden, Parameter, Felder, Annotationen, Klassen und Dateien zu generieren. Im Kern sind die genutzten SpecObjekte aber unveränderlich und können so partiell und mehrfach genutzt und mit neuen Informationen abgeleitet werden. Dabei wird soweit wie möglich mit typsicheren Konstanten und Literalen (z. B. Klassenliterale) gearbeitet: www.javaspektrum.de helloWorld) .build(); javaFile.wr iteTo(System.out); Für Methodenrümpfe und Ausdrücke werden Strings für Code-Fragmente genutzt. Um den Komfort dabei zu erhöhen, gibt es auch da eine kleine fluent DSL, die Ausdrücke zusammenstellt und sich beispielsweise um Einrückungen, Umbrüche und Semikolons kümmert: private MethodSpec computeRange(String name, int from, int to, String op) { return MethodSpec.methodBuilder(name) .returns(int.class) .addStatement("int result = 0") .beginControlFlow("for (int i = $L; i < $L; i++)", from, to) .addStatement("result = result $L i", op) .endControlFlow() .addStatement("return result") .build(); } In Code-Strings kann man semantische Platzhalter nutzen, die unterschiedlich interpretiert werden. Somit kann eine Typprüfung mit den übergebenen Parametern vorgenommen werden. H $L für Literale, H $S für Strings mit Anführungszeichen, Escapes und Umbrüchen, H $T für Typen aus Klassenliteralen oder ClassName-Definitionen, mit automatischer import-Deklaration am Dateianfang, H $N wird genutzt, um auf Namen anderer Elemente (Spec-Objekte) der generierten Klasse oder Methode zuzugreifen, Hfür den Quellcode eines Spec-Objektes benutzt man dieses ebenfalls mit $L. JavaPoet unterstützt auch die Erzeugung von Enums und inneren anonymen Klassen. Ich persönlich fände es schön, wenn JavaPoet einige APIBequemlichkeiten mitbringen würde, das würde duplikaten Code einsparen. Zum Beispiel TypeSpec.publicClassBuilder, addPrivateMethod, addPrivateFieldWithGetter, addIfStatement usw. @RuntimeType public static Object intercept(@SuperCall Callable<?> superMethod, @Origin Method method) throws Exception { // Abfangen des Aufrufs und Prüfung der Zugriffsrechte Role role = method.getAnnotation(Secured.class).requiredRole(); if (currentUser.hasRole(role)) return superMethod.call(); throw new SecurityException(method, role, user); } // public static void main(String[] args) // { System.out.println("Hello JavaPoet!"); } MethodSpec main = MethodSpec.methodBuilder("main") JavaFile javaFile = JavaFile.builder("com.example.helloworld", 5 CGLib CGLib ist eine schon etwas in die Jahre gekommene, abstraktere Programmierschnittstelle, um Bytecode zu erzeugen. Sie wurde bisher zum Beispiel in Hibernate für das Anreichern von Entitäten, für das dynamische Nachladen und andere Funktionalitäten genutzt. Der häufigste Anwendungsfall ist die Erzeugung von Subklassen existierender Klassen, in denen Verhalten von nichtfinalen Methoden verändert wird, ähnlich wie bei (dynamischen) Proxies, bei denen CGLib diverse Anleihen nimmt. Mittels Enhancer-API ist das relativ einfach möglich: 57 Effective Java // welche Klasse soll abgeleitet werden [DynamicProxyTutorialDW] B. Goetz, Java theory and practice: Decorating with dynamic proxies, 2005, enhancer.setSuperclass(MyFormatter.class); http://www.ibm.com/developerworks/library/j-jtp08305/ // welcher callback für alle Methoden [DynamicProxyTutorialJJ] J. Jenkov, Java Reflection – Dynamic Proxies, 2014, Enhancer enhancer = new Enhancer(); // hier mit festem Rückgabewert enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { if(method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) { return "Fixed Format"; } else { return proxy.invokeSuper(obj, args); } } }); Formatter proxy = (Formatter) enhancer.create(); proxy.format(new Date()) -> "Fixed Format" http://tutorials.jenkov.com/java-reflection/dynamic-proxies.html [GroovyMetaProg] http://www.groovy-lang.org/metaprogramming.html [Hung14] M. Hunger, Dynamische Compiler mit Graal und Truffle, in: JavaSPEKTRUM, 06/2014 [Janino] http://unkrig.de/w/Janino [JaninoTutorial] T. Gibara, Tackling Java Performance Problems with Janino, 2007, https://today.java.net/pub/a/today/2007/02/15/ tackling-performance-problems-with-janino.html [JavaassistTutorial] http://jboss-javassist.github.io/javassist/tutorial/tutorial.html [JavaCompiler] http://docs.oracle.com/javase/7/docs/api/javax/tools/JavaCompiler.html Neben diesem mächtigen, aber aufwendigen MethodInterceptor gibt es für andere Einsatzfälle alternative Callbacks, wie den effizienten InvocationHandler sowie den simplistischen FixedValueCallback. Die Dokumentation für CGLib ist ziemlich spärlich. Lustigerweise stammt die ausführlichste Beschreibung, das „missing Manual“ aus 2013, von Rafael Winterhalter [CGLibTutorial], dem Autor von ByteBuddy. Fazit Es gibt noch weitere Ansätze, wie die modellgetriebene Softwareentwicklung, die aus detailliert spezifizierten Modellen große Teile des Basisquelltexts (und andere Artefakte) eines Projekts generiert, der dann mittels Konfiguration, Ableitung oder Delegation konkretisiert wird. Das geht jedoch weit über das hinaus, was ich hier vorstelle. Wie jede andere Methode sollte man Codegenerierung bewusst nur dann einsetzen, wenn es wirklich notwendig ist und der Nutzen den Aufwand weit überwiegt. Dessen Einsatz zu übertreiben schadet eher, als dass es hilft. Ein wichtiger Aspekt ist die Wartbarkeit. Niemand will generierten Code warten. Daher sollte dieser in jedem Build neu erzeugt und nicht in die Versionsverwaltung eingecheckt werden. [JavaPoetAnkündigung] https://corner.squareup.com/2015/01/javapoet.html [JavaPoetDokumentation] https://github.com/square/javapoet/blob/master/README.md [KabRup15a] H. M. Kabutz, S. Ruppert, Es werde Code! Code statt dynamischer Proxies generieren, 2015, https://jaxenter.de/es-werde-code-13426 [KabRup15b] H. M. Kabutz, S. Ruppert, Dynamic Proxies, entwickler.press, 2015 [Kabutz10] H. M. Kabutz, Generieren von Klassen – Teil 1 und 2, in: JavaSPEKTRUM, 03und 04/2010 [Kabutz10a] H. M. Kabutz, Generating Static Proxy Classes – 1/2, The Java Specialists' Newsletter, 19.2.2010, http://www.javaspecialists.eu/archive/Issue180.html [Kabutz10b] H. M. Kabutz, Generating Static Proxy Classes – 2/2, The Java Specialists' Newsletter, 1.3.2010, http://www.javaspecialists.eu/archive/Issue181.html [Müller14] B. Müller, Bytecode, Class-Loader und Class-Transformer, Folien zum Vortrag vom 14.8.2014, http://www.jug-ostfalen.de/assets/wp/2014/08/jug-classloader.pdf [OracleJavaProxy] http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html [Winterhalter14a] R. Winterhalter, How to make Java more Dynamic with Runtime Code Generation, 1.7.2014, http://zeroturnaround.com/rebellabs/how-to-make-java-more-dynamicwith-runtime-code-generation/ Links [Winterhalter14b] R. Winterhalter, How my new Friend Byte Buddy enables annotation-driven Java runtime code generation, 8.7.2014, http://zeroturnaround.com/rebellabs/how-my-new-friend- [AnnotationProcessing] byte-buddy-enables-annotation-driven-java-runtime-code-generation/ https://deors.wordpress.com/2011/10/31/annotation-generators/ [ASMIntro] D. R. Chawdhuri, Manipulating Java Class Files with ASM 4 – Part One: Hello World!, 2012, http://www.javacodegeeks.com/2012/02/manipulating-java-class-fileswith-asm.html [ASMTutorial] E. Bruneton, ASM 4.0 A Java bytecode engineering library, 2007/2011, http://download.forge.objectweb.org/asm/asm4-guide.pdf [AspectJ] The AspectJTM Programming Guide, https://www.eclipse.org/aspectj/doc/released/progguide/ [ByteBuddy] Tutorium, Security Library, http://bytebuddy.net/#/tutorial [CGLibTutorial] R. Winterhalter, cglib: The missing manual, 2013, https://github.com/cglib/cglib/wiki/Tutorial oder Michael Hunger (Twitter @mesirii) interessiert sich als IT-Consultant für alle Belange der Softwareentwicklung, vor allem Sprachen (Java.next, DSLs) und Code-Qualität. Er arbeitet(e) an mehreren OpenSource-Projekten mit, ist Autor, Editor, Buch-Reviewer und Sprecher bei Konferenzen. E-Mail: [email protected] http://www.javacodegeeks.com/2013/12/cglib-the-missing-manual.html [Dari15] Th. Darimont, Beschleunigung dynamischen Codes mit Bytecode-Generierung und Spring-Ausdrücken, in: JavaSPEKTRUM, 03/2015 58 JavaSPEKTRUM 6/2015