In meinem Artikel High Performance R-Syslog hatte ich das erste Mal die Notwendigkeit gesehen, über gängige Templating-Engines hinauszugehen und aufgrund der Last eine optimierte Implementierung zu erstellen. Das Thema fand ich auch hernach noch so interessant, dass ich mich ein wenig mehr damit beschäftigt habe. Schließlich, dachte ich, ist es doch sehr nützlich, eine Möglichkeit für Templating zu haben, die so schnell wie irgend möglich ist. Nachdem ich diese Funktion dann auch in einem zweiten Projekt gebraucht hatte, befand ich, dass es Zeit war, eine wiederverwendbare Komponente dafür zu erzeugen. Und bei Java läuft das auf eine Library hinaus. Also habe ich nochmal reviewt, was für den enormen Performance-Boost beim R-Syslog-Projekt geführt hat: Das Vorkompilieren von Templates in "Teile", die dann beim Verarbeiten des Templates ausgewertet und in einem StringBuilder konkateniert werden. Das Template "Hallo $(welt)!" zerlegt sich folglich in die Teile [Text "Hallo ", Platzhalter "welt", Text "!"].
Ferner ist mir aufgefallen, dass man eigentlich nicht zwischen einem Text-Teil und einem Platzhalter-Teil unterscheiden muss. Denn wenn jeder Teil schließlich zu einem Text ausgewertet wird, dann hätte ein Text-Teil nur als Implementierung dieser Auswertung die Identität. In Code gesprochen:
class TextPart extends Part {
String text;
@Override
String evaluate(TemplateProcessingContext c) { return text; }
}
class Placeholder extends Part {
String name;
String evaluate(TemplateProcessingContext c) {
return c.getActualValueFor(name);
}
}
Zerlegt man ein Template nun also einmalig bei Applikationsstart in eine Liste von Teilen, dann lässt sich der tatsächliche Text erzeugen, indem man in einem StringBuilder die einzelnen Ergebnisse der evaluate-Methode konkateniert.
Den Prozess des Zerlegens eines Templates in diese Teile habe ich dann 'kompilieren' genannt. Dabei gehe ich tatsächlich einfach nur mit einem Regex
\$\(([^\)])\)
über den Template-Quelltext und suche mir alle Vorkommen der Platzhalter. Dann erzeuge ich abwechselnd einen TextPart und einen PlaceholderPart. Die Ergebnisliste ist dann ein kompiliertes Template.
Und das ist eigentlich schon die ganze Magie. Das reicht lange nicht, um als General-Purpose-Template-Engine zu taugen, aber es ist gerade durch diese Einfachheit auch extrem schnell. Ohne Anspruch auf Vollständigkeit würde ich aber von einer allgemeingültigen Template-Engine noch erwarten, folgendes zu können:
- internationalisierte Texte einsetzen
- auf Elemente einer Struktur zugreifen
- rekursiv in Strukturen eintauchen
- Mengen durchsuchen bzw. durchiterieren
- Code einbetten und auswerten
- die Template-Sprache in andere Sprachen einbetten, wie xml oder html
- kontextsensitiv Ausschnitte selbstständig anpassen (z.b. relative Hyperlinks zu absoluten auflösen)
- eine einfache Integration mit bestehenden, verbreiteten Frameworks, wie Spring, Quarkus, Micronaut o.ä. bieten
All das bietet meine Implementierung nicht, aber das ist für den Anwendungsfall, den ich im Sinn hatte, auch gut so. Denn je mehr Komplexität abgebildet würde, desto langsamer würde die Engine zwangsweise auch. Wenn man solche Funktionen sucht, dann gibt es dafür eh auch schon genügend bekannte Vertreter, wie thymeleaf, FreeMarker, Pebble oder JMustache. (Apache Velocity führe ich hier explizit nicht auf, da das Projekt von Spring bspw. entfernt wurde).
Schlussendlich konnte ich meine Erfahrung aus dem R-Syslog-Projekt so sinnvoll externalisieren und mir für weitere Anwendungsfälle bereitlegen.
Meine Implementierung steht übrigens auf meinem GitLab unter Apache Lizenz 2.0 zur Verfügung und kann von dem dort angegebenen Maven-Repository bezogen werden.