Ist jte eine Alternative zu JSP?

Head of Backend Development @ Chrono24

Bei Chrono24 sind alle SEO relevanten Seiten serverseitig gerendert. Als Template Engine kommt hierfür seit 2010 JavaServer Pages (kurz: JSP) zum Einsatz.

JSP ist eine generische Template Engine und kann beliebige Textdokumente generieren. Dabei wird JSP gerne als deprecated oder als Legacy angesehen. Doch das stimmt so nicht, denn das Projekt wird durchaus noch gepflegt und ist über die Jahre ausgereift. Die Sprache ist stabil – it just works.

In jeder Sprache lässt sich schöner und nicht schöner Code schreiben, dasselbe gilt auch für JSP. Es gibt in der Praxis aber Probleme, die sich ohne fundamentale Anpassungen an JSP nicht beheben lassen. In diesem Blog-Beitrag möchten wir Euch diese Probleme aufzeigen und eine mögliche Lösung mit Euch teilen.

Kein automatisches Output Escaping

JSP ist eine generische Template Engine und kann beliebige Textdokumente generieren. Wir verwenden JSP allerdings ausschließlich zum Erzeugen von HTML Dokumenten. JSP versteht kein HTML und kann daher auch nicht entscheiden, wie Ausgaben escaped werden müssen. Output muss daher manuell escaped werden, was aufwändig, fehleranfällig und schlechter lesbar ist.

<%-- This is open for XSS attacks --%>
<p>${user.name}</p>

<%-- This prevents XSS attacks --%>
<p>${c24:escapeHtml(user.name)}</p>

Expression Language

JSP kompiliert zu nativem Java Code. Das ist exzellent für die Performance. Expression Language (kurz: EL) Ausdrücke werden aber zur Laufzeit interpretiert und sind dadurch langsam. Entwickler müssen die EL als eine weitere Sprache beherrschen, die immer wieder für Überraschungen sorgt. So werden zum Beispiel Default-Interface-Methoden nicht über die Property Schreibweise aufgelöst und Enum-Werte müssen als String und ohne Autocompletion eingegeben werden. Da EL dynamisch ist, fehlt die ansonsten von Java gewohnte Typsicherheit. Es kommt daher vor, dass eine Property nicht in allen Situationen existiert. Das fällt allerdings erst zur Runtime statt zur Compile Time auf, im schlimmsten Fall erst auf dem Live-System.

Logik für optimierte Ausgabe

Es muss viel JSP-Logik geschrieben werden, um eine optimierte Ausgabe von CSS-Klassen oder Data Attributen zu erzielen.

Solche Stellen finden sich häufig in der Code Basis:

<a<c:if test="${not empty name}"> data-name="${name}"</c:if>>Click</a>

Auch das ist aufwändig, fehleranfällig und schwer lesbar, vor allem wenn es mehrere kombinierbare Attribute sind.

Globale Variablen

JSP Includes sind einfache Textersetzungen. Wird ein Include an mehreren Stellen verwendet, wird es schwer zu überblicken, ob verwendete Variablen in allen aufrufenden Dateien verfügbar sind. Refactorings werden dadurch aufwändiger.

Die Alternative zu Includes sind Tags. Diese sind parametrisiert und es kann auch nur auf die übergebenen Attribute zugegriffen werden. Die Performance ist in der Regel schlechter als bei Includes, dafür können keine Variablen von außen in das Tag leaken. Die Deklaration der Attribute ist etwas umständlich und es gibt keine Unterstützung für Default-Werte von Attributen.

JSP Alternativen

Für Java gibt es eine große Auswahl an Template Engines. Wir haben einige davon betrachtet, ob sie als JSP Alternative geeignet sind.

1. Thymeleaf

Thymeleaf hat den Vorteil, dass es weit verbreitet und etabliert ist. Außerdem ist es die Standard Engine für Spring. Es gibt ein IntelliJ Plugin, was die tägliche Arbeit erleichtert und Refactorings ermöglicht. Thymeleaf hat kontext-sensitives Output Escaping. Allerdings gibt es zwei Dialekte – Standard und Spring – was immer wieder für Unklarheiten sorgt. Die Syntax ist sehr komplex und man muss viele neue Konzepte lernen, bis man mit Thymeleaf produktiv ist. Anders als JSP ist die Sprache komplett interpretiert und Thymeleaf ist die Java Template Engine mit der schlechtesten Performance.

Java Template Engine Benchmark
Java Template Engine Benchmark

2. Pebble

Pebble ist ebenfalls eine sehr populäre Template Engine, mit einer einfacheren Syntax als Thymeleaf. Diese Engine ist ebenfalls interpretiert, aber bereits deutlich performanter als Thymeleaf. Allerdings hat Pebble kein automatisches Output Escaping und ist nicht typsicher. Wie bei JSP fallen Typfehler erst zur Laufzeit auf. Das IntelliJ Plugin ist exzellent, allerdings müssen Typinformationen durch Kommentare hinzugefügt werden.

3. Rocker

Rocker ist eine relativ neue Template Engine, die komplett nach Java compiliert und typsicher ist. Die Performance ist exzellent. Allerdings gibt es kein IntelliJ Plugin für Rocker und kontext-sensitives Output Escaping wird nicht unterstützt. Trotzdem ist Rocker am nähesten an unserer Wunschvorstellung von einer Template Engine.

Zwischenfazit

Es gibt aktuell keine moderne Template Engine für Java, die alle unsere Anforderungen abdeckt. Vermutlich liegt das daran, dass viele neue Projekte auf clientseitiges Rendering (vue / react) setzen und sich darauf inzwischen auch die Framework-Entwicklung konzentriert. Bei Chrono24 verwenden wir Vue JS bereits für Seiten mit Applikationscharacter. Für alle SEO relevanten Seiten ist serverseitiges Rendering allerdings Pflicht.

Eine moderne Template Engine für Java

Während des ersten Lockdowns 2020 haben wir das Experiment gewagt, eine eigene Open Source Engine zu entwickeln. Die grundlegende Idee war simpel: EL Expressions durch nativen Java Code ersetzen.

Dadurch ergeben sich einige Vorteile:

  • Ausgezeichnete Performance
  • Sehr schlanker Engine Core
  • Keine zusätzliche Sprache für Expressions
  • Typsicherheit
  • Code Completions und Refactorings durch IntelliJ

jte (Java Template Engine) versucht die Probleme, die wir mit JSP haben, zu lösen und dabei die Stärken von JSP beizubehalten.

Output Escaping

jte bietet kontextsensitives Output Escaping. Je nachdem, in welchem Template-Slot die Variable ausgegeben wird, wird diese entsprechend escaped. Die Analyse des HTML geschieht zur Compile Time, führt also zu keinem Overhead zur Runtime. Als Library für das Output Escaping wird der OWASP High Performance Java Encoder verwendet.

@param String name

Hello, ${name}

jte wird den Inhalt der Variable name automatisch escapen. Wäre der Inhalt <script>, würde der Output sein: Hello, &lt;script&gt;

@param String name

<a data-name="${name}">${name}</a>

Hier unterscheidet jte: Die Variable im HTML-Attribut wird anders escaped, als die Variable im Tag Body. Unterstützt werden folgende Slots:

  • Tag Body
  • Tag Attribut
  • JavaScript
  • CSS

Außerdem wird zur Compile Time geprüft, dass Output nur in erlaubte Slots geschrieben wird.

Expression Language

Anstatt einer eigenen Expression Language, verwendet jte Java Code. Das macht die Engine-Entwicklung sehr einfach, da keine Ausdrücke transformiert werden müssen. Entwickler sehen genau den Code, der am Ende auch ausgeführt wird. Es gibt nur einige wenige Schlüsselwörter, die erlernt werden müssen. Diese orientieren sich so stark wie möglich an der entsprechenden Java Syntax.

Für das Rendern einer Seite kann z.B. ein Page Object an das Template übergeben werden:

@import org.example.WelcomePage

@param WelcomePage page

<html>
  <body>
    <h1>${page.getTitle()}</h1>

    @if(page.getLoggedInUser() != null)
       Hello ${page.getLoggedInUser().getName()}
    @else
       Welcome, traveller.
    @endif
  </body>
</html>

Hier wird eine Klasse mit @import importiert. Parameter werden mit @param übergeben. @if, @else, @endif orientiert sich stark an den aus Java bekannten Strukturen.

Möchte man das Grundgerüst dieses Templates auf mehreren Seiten wiederverwenden, kann ein Tag herausgezogen werden. Angenommen, es existiert eine Page Basisklasse mit einer getTitle() Methode, kann die Datei layout/main.jte so angelegt werden:

@import org.example.Page
@import gg.jte.Content

@param Page page
@param Content content

<html>
  <body>
    <h1>${page.getTitle()}</h1>

    ${content}
  </body>
</html>

Durch den Einsatz des Layouts ist die Welcome Page stark vereinfacht:

@import org.example.WelcomePage

@param WelcomePage page

@layout.main(page = page, content = @`
    @if(page.getLoggedInUser() != null)
       Hello ${page.getLoggedInUser().getName()}
    @else
       Welcome, traveller.
    @endif
`)

Etwas gewöhnungsbedürftig ist die Erzeugung von gg.jte.Content. Die Idee ist hier, dass diese wie Strings durch @`` erzeugt werden können, im Gegensatz zu Strings aber selbst Template Ausdrücke beinhalten können und auch nicht escaped werden. Durch den Compiler werden Content-Blöcke in effiziente Lambdas übersetzt.

Logik für optimierte Ausgabe

Möchte man einen möglichst schlanken Output, muss bei JSP viel von Hand gemacht werden. Dadurch sind Templates oft unnötig kompliziert.

<a data-name="${name}">Click</a>

Ist ${name} null, dann ist die JSP Ausgabe: <a data-name="">Click</a>

Möchte man das Attribut für diesen Fall weglassen, muss man das von Hand tun:

<a<c:if test="${not empty name}"> data-name="${name}"</c:if>>Click</a>

Insbesondere bei mehreren solcher Attribute wird das Template dadurch schnell unübersichtlich.

jte kann diesen manuellen Schritt direkt zur Compile Time übernehmen. Der entsprechende jte Code sieht dabei so aus wie der nicht optimierte JSP Code:

<a data-name="${name}">Click</a>

Und für einen Parameter name = null, wäre der Output dann: <a>Click</a>

Analog arbeitet jte bei Boolschen HTML-Attributen, wie zum Beispiel required.

Globale Variablen

Template Code wird wiederverwendet, indem man diesen in ein Tag extrahiert. Im Gegensatz zu JSP,  haben jte Tags nahezu keinen Overhead. Sie werden zu einem simplen Methodenaufruf kompiliert.

Außerdem können Parameter sehr einfach mit Defaultwerten versehen werden:

@param String name
@param boolean anonymise = false

@if(anonymize)
  Anonymous
@else
  ${name}
@endif

Für einige Dinge sind die aus JSP bekannten “globalen” Variablen sinnvoll, damit diese nicht durch alle Tags durchgereicht werden müssen.

Einen RequestContext mit diesen Variablen realisieren wir durch einen ThreadLocal, der direkt nach dem Rendern zurückgesetzt wird. Dieser sollte sich daher in Zukunft sehr gut für die mit Project Loom kommenden Scope Variables eignen.

import gg.jte.Content;

public final class JteContext {
  public static Content localize( String key, Object ... params ) {
    return getContext().getLocalizer().localize(key, params);
  }

  // ThreadLocal context...
}

Auf diesem Weg lässt sich analog zu fmt:message in JSP sehr einfach eine Lokalisierung der Templates durchführen und Chrono24 wäre auch mit jte in 22 Sprachen verfügbar.

@import static c24.jte.JteContext.*

@param Page

<p>${localize("common.greeting", page.getLoggedInUser().getName())}</p>

Die localize() Methode gibt keinen String, sondern das Interface gg.jte.Content zurück. Java Implementierungen von diesem Interface sind programmatische Template-Blöcke, ähnlich zu JSPs javax.servlet.jsp.tagext.TagSupport. Für die Lokalisierung wird damit erreicht, dass der vom Übersetzer gepflegte Text in Lokalisierung Keys nicht escaped wird, sondern nur die übergebenen Parameter.

Ausblick

jte hat inzwischen die Version 1.5 erreicht und wird bereits in privaten Projekten eingesetzt. Mit dem IntelliJ Plugin macht es inzwischen schon ziemlich Spaß, an jte Templates zu arbeiten.

IntelliJ Plugin
IntelliJ Plugin

Das Plugin kann außerdem JSP Code in jte-Code konvertieren. Nach der Konvertierung müssen einzig die EL Expressions noch nach Java portiert werden. Durch ein Bridging Tag können konvertierte jte Tags von JSP aus aufgerufen werden, so dass eine schrittweise Migration möglich ist.

Momentan evaluieren wir den Einsatz von jte, indem wir einige wenige, ausgewählte JSPs portieren und ganz genau schauen, ob sich dadurch tatsächlich Fehler vermeiden lassen, und ob mit jte die Produktivität und der Spaß am Entwickeln steigt. Wir sind gespannt …