Template TagÄnderungen:
EinführungOft erzeugen JSP Tags HTML Code, der dann sozusagen das Tag in der resultierenden HTML Seite der JSP ersetzt. Dies wird normalerweise so gemacht (Beispiel für ein hidden field): public int doStartTag() throws JspException { try { pageContext.getOut().print("<input type='hidden' value='" + value + "'>"); } catch (IOException e) { ... } } Obwohl die Aufgabe sehr simpel ist, sieht der code sehr hässlich aus. Bei komplexeren Aufgaben wird der Code noch hässlicher. Ausserdem erinnert es stark daran, wie man früher HTML code direkt in den code von servlets hineinprogrammiert hat und sich am Anfang auch nichts dabei gedacht hat. Benutzen tut man das dann so: ... <tl:hiddenField value="hallo"/> ... Jedenfalls habe ich in meinen Projekten immer wieder den Fall, dass viele Jsp Tags einfach nur bestimmte HTML Fragmente erzeugen, die eigentlich immer gleich aussehen und sich nur an ein paar wenigen Stellen unterscheiden, nämlich dort, wo die Daten in den Text hineingesetzt werden müssen. Ein template also. Irgendwann kam ich auf die Idee, da dies eigentlich immer gleich funktioniert, dass es eigentlich keinen Sinn macht jeweils ein Tag für jeden Spezialfall zu programmieren. Viel besser wäre es doch, wenn man an ein generisches Template Tag einfach einen Text übergeben könnte und die dynamischen Teile mit Variablen (${value}) definieren könnte. Da der Text unter Umständen länger werden kann macht es vermutlich Sinn ihn gleich in eine Datei zu schreiben: <input type="hidden" value="${value}"> Wenn man dem Template Tag jetzt noch den Dateinamen mitteilt, sollte es also in der Lage sein den Text aus der Datei zu lesen und darzustellen. Dummerweise gibt es aber noch zwei Haken: Unsere Variable ${value} wird zum einen nicht ersetzt und zweitens wüssten wir nicht einmal mit was, da wir noch keine Möglichkeit geschaffen haben, Daten an unser Tag zu übergeben. Ungefähr zu diesem Zeitpunkt bekam ich das Gefühl, dass man dies nicht selbst tun sollte, sondern besser von einer existierenden Template engine, wie zum Beispiel Apache Velocity oder Freemarker erledigen lassen sollte. Das habe ich dann auch getan, und je ein Tag für Velocity und eins für Freemarkter erstellt. Der Rest dieses Beitrags erlätert, wie ich dies getan habe. Das fertige Tag und eine Beispielapplikation steht selbstverständlich als download zur Verfügung. Auf Velocity und Freemarker gehe ich nicht weiter ein, da beide Projekte gut dokumentiert sind. Ob man jetzt Velocity oder Freemarker benutzt ist wohl in den meisten Fällen Geschmacksache. Persönlich bevorzuge ich im Moment Freemarker, da es erstens besser dokumentiert ist, und laut dieser mehr Möglichkeiten bietet. Erstellen des Tags für VelocityWas benötigt unser Tag, um arbeiten zu können? Als erstes sicherlich den Pfad zur Templatedatei und dann die Daten. Das erste sollte kein Problem sein indem man dem Tag einfach ein Attribut mit dem Dateinamen verpasst. Bei den Daten muss man sich überlegen, in welcher Form man die Daten übergeben will. Man könnte zum Beispiel ein bean übergeben. Falls man jedoch mehrere Beans übergeben will wird das schwierig. Daher bietet sich eher an, eine java.util.Map zu übergeben, sodass beliebig viele Objekte als Daten übergeben werden können. Velocity und Freemarker nehmen übrigens ebenfalls eine Map, die dort normalerweise Context heisst entgegen, sodass das ganze gut zusammenpasst. Unsere tld Definition sieht also folgendermassen aus: ... <tag> <name>velocity</name> <tag-class>tag.VelocityTag</tag-class> <body-content>JSP</body-content> <description> </description> <attribute> <name>resourcePath</name> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>context</name> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag> ... Wie man sieht habe ich jetzt einfach mal mit dem Tag, das die Velocity Template Engine implementiert begonnen. Statt filename habe ich mich für resourcePath entschlossen, um mir nicht die Möglichkeit zu verbauen später Dateien zum Beispiel per classpath zu laden. Die Daten, werden im Attribut context, das wie gesagt vom Typ java.util.Map ist übergeben. Der Java code des Tags, den ich hier nur verkürzt darstelle sieht folgendermassen aus: ... public class VelocityTag extends TagSupport { ... private String resourcePath; private Map context; ... public int doStartTag() throws JspException { try { VelocityContext vc = new VelocityContext(context); Velocity.mergeTemplate(resourcePath, pageContext.getResponse().getCharacterEncoding(), vc, pageContext.getOut()); } catch (Exception e) { throw new JspException(e); } return super.doStartTag(); } ... Die Daten werden in einen sogenannten VelocityContext gestellt. Dieser wird dann zusammen mit dem resourcePath an die Funktion mergeTemplate übergeben, die die Daten in's Template setzt und das Ergebnis auf den JspWriter ausgibt. Damit auch das Encoding stimmt, wird das Encoding der JSP an die Velicity Engine weitergegeben. Am Rande noch ein Hinweis zur Performance: da der outputstream der page mit pageContext.getOut() ebenfalls übergeben wird, kann die TemplateEngine übrigens direkt auf diesen outputstream schreiben. Sie muss deshalb nicht erst einen extra String erzeugen, der dann sowieso wieder an den outputstream geschickt werden müsste. Dadurch wird mit memory und CPU sparsam umgegangen. In der JSP kann dieses Tag jetzt folgendermassen benutzt werden: <%@ page import="java.util.Map, java.util.HashMap"%> <%@ taglib prefix="tmpl" uri="template" %> <html> <body> <b>Velocity example</b> <% Map map = new HashMap(); map.put("productname", "car"); map.put("price", "26'000 EUR"); %> <tmpl:velocity resourcePath="WEB-INF/template/product.vm" context="<%= map %>"/> </body> </html> Und mit dem Template WEB-INF/template/product.vm Product: $productname, Price: $price Sieht die Seite im Browser dann so aus: ![]() Normalerweise würde man die Map nicht in der JSP erzeugen. Dies wird hier jedoch wegen der einfacheren Verständlichkeit gemacht. FreemarkerDas Freemarkertag unterscheidet sich nicht gross vom Velocity Tag. Im tld ändert sich ausser dem Namen nichts. Der Hauptunterschied besteht in der Implementierung von doStartTag(): ... public class FreemarkerTag extends TagSupport ... public int doStartTag() throws JspException { Configuration cfg = (Configuration)pageContext.getServletContext().getAttribute( FreemarkerInitializationServlet.FREEMARKER_CONFIGURATION); try { Template temp = cfg.getTemplate(resourcePath); temp.setEncoding(pageContext.getResponse().getCharacterEncoding()); temp.process(context, pageContext.getOut()); } catch (Exception e) { throw new JspException(e); } return super.doStartTag(); } Wie man sieht wird als erstes eine Referenz auf die Freemarker Configuration geholt. Wie diese initialisiert wird beschreibe ich später. Danach holt man sich das gewünschte template von der Konfiguration, übergibt wie gehabt das encoding, lässt danach das template mittels process() die Daten einbinden und schickt das Ergebnis zum JspWriter. Unsere freemarker.jsp unterscheidet sich von velocity.jsp nur an einer Stelle: ... <tmpl:freemarker resourcePath="WEB-INF/template/product.ftl" context="<%= map %>"/> .. Und mit dem Template WEB-INF/template/product.ftl Product: ${productname}, Price: ${price} Sieht die Seite im Browser dann so aus: ![]() InitialisierungSowohl für Freemarker als auch für Velocity benötigt man eine Initialisierung. Beide Templateengines arbeiten nämlich sowohl im Webumfeld als auch ohne. Deshalb muss man ihnen sagen, wo das Verzeichnis ist, in dem die templatefiles liegen. Die Initialisierung habe ich mit Servlets gemacht. Für Velocity sieht das so aus: public class VelocityInitializationServlet extends HttpServlet { public void init(ServletConfig servletConfig) throws ServletException { super.init(servletConfig); try { Properties properties = new Properties(); ServletContext sc = getServletContext(); String rp = sc.getRealPath("/"); properties.put(Velocity.FILE_RESOURCE_LOADER_PATH, rp); Velocity.init(properties); } catch (Exception e) { throw new NestableRuntimeException(e); } } } Achtung: um richtig J2EE konform zu sein ist es vermutlich besser den ClasspathResourceLoader zu verwenden, wie das die Velocity Dokumentation vorsieht. Die template files müssen sich dann in WEB-INF/classes oder einem jar in WEB-INF/lib befinden. Obwohl das für die endgültige Version richtig sein mag, finde ich es für die Entwicklung ein wenig umständlich. Deshalb verwende ich den Ansatz mit sc.getRealPath(). Aber vermutlich werde ich dies in Zukunft doch ändern, um den von Velocity vorgeschlagenen Weg einzuhalten. Für Freemarker sieht das ganze sehr ähnlich aus: public static final String FREEMARKER_CONFIGURATION = "FREEMARKER_CONFIG"; public void init(ServletConfig servletConfig) throws ServletException { super.init(servletConfig); try { Configuration cfg = new Configuration(); ServletContext sc = getServletContext(); cfg.setServletContextForTemplateLoading(sc, "/"); sc.setAttribute(FREEMARKER_CONFIGURATION, cfg); } catch (Exception e) { throw new NestableRuntimeException(e); } } Allerdings sieht man gleich, dass man bei Freemarker mit setServletContextForTemplateLoading daran gedacht hat, dass Resourcen im Webumfeld eigentlich über ServletContext.getResourceAsStream() geladen werden können, was das ganze doch flexibler macht, als bei Velocity. OptimierungenIch möchte den Code hier zugunsten der einfachen Verständlichteit nicht weiter ausbauen. Diser Beitrag, soll den Stil eines Tutorials haben, das den Weg von der Idee bis zur funktionierenden Umsetzung aufzeigen soll. Nichtsdestotrotz möchte ich ein paar Optimierungsmöglichkeiten aufführen:
Ich werde diese Optimierungsmöglichkeiten implementieren und in einem separaten download zur Verfügung stellen. |