07.10.2012
EJB TimerService (Enterprise Java Beans)
In dem folgenden Artikel wird der TimerService von EJBs erklärt. D.h. es wird beschrieben, wie ein TimerService eingesetzt werden kann, um regelmäßig eine Methode eines Stateless Session Beans aufzurufen. Dabei wird der TimerService per Annotation @TimerService
definiert. Eine programmatische Lösung wäre aber auch möglich gewesen. Zum besseren Verständnis wird als Beispiel ein einfacher JEE6-RSS-Reader erstellt, der in regelmäßigen Abständen einen RSS-Feed abfragt und das Ergebnis darstellt.
Beispielanwendung
Im folgenden Beispiel wird ein RSS-Reader erstellt. Es wird eine Stateless Session Bean mit einem TimerService implementiert, die alle 15 Minuten verschiedene RSS-Feeds abfragt und in einer Datenbank speichert. Dabei werden nur der Titel, der Link zu dem Artikel, das Datum und die Quelle gespeichert. Außerdem wird eine zweite Stateless Session Bean erstellt, die über eine JSF-Seite angesprochen wird und die Werte aus der Datenbank ausliest und in einer dataTable
anzeigt. Die dataTable
wird mittels einer css-Datei (Cascading Style Sheet) formatiert dargestellt.
Die Idee hinter der Anwendung ist, dass die RSS-Feeds nicht von jedem Benutzer einzeln beim Feed-Provider abgefragt werden müssen, sondern dass dies an einer zentralen Stelle passiert. Der User fragt, dann nur noch die Datenbank ab. Ein weiterer Vorteil dieser "Architektur" ist, dass die Daten persistent gespeichert bleiben. RSS-Provider haben die Angewohnheit nur die letzten 10-30 Nachrichten in ihren Feed zu schreiben. Wenn man die Feeds nicht regelmäßig abfragt, gehen eventuell wichtige Nachrichten verloren.
Die folgende Abbildung zeigt eine Architekturübersicht:

Persistence mit JPA 2.0
Als erstes wird eine Entity-Klasse erstellt, die eine einzelne RSS-Message speichert. Die Klasse RSSMessage
hat folgende Membervariablen link, messageDate, title, source
, wobei der Link als Primary Key dient. Außerdem wird noch eine @NamedQuery
definiert, damit die RSSMessage
s später einfacher ausgelesen werden können. Zu beachten ist außerdem, dass messagDate
vom Datentype Date
ist und mit der Annotation zus�tzlich als Datum gekennzeichnet ist.
package org.hameister.rss; import java.io.Serializable; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.NamedQuery; import javax.persistence.Temporal; import javax.persistence.TemporalType; /** * * @author Hameister */ @Entity @NamedQuery(name="findAllRssMessages", query="SELECT message FROM RSSMessage message") public class RSSMessage implements Serializable { private static final long serialVersionUID = 1L; @Id private String link; @Temporal(TemporalType.DATE) private Date messageDate; private String title; private String source; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getLink() { return link; } public void setLink(String link) { this.link = link; } public Date getMessageDate() { return messageDate; } public void setMessageDate(Date date) { this.messageDate = date; } public String getSource() { return source; } public void setSource(String source) { this.source = source; } }
Wenn man die Entity-Klasse ohne Wizard anlegt, dann darf die Datei persistence.xml
nicht vergessen werden.
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> <persistence-unit name="RSSReaderTimerServicePU" transaction-type="JTA"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/sample</jta-data-source> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/> </properties> </persistence-unit> </persistence>
Stateless Session Bean mit TimerService
Als nächstes wird eine Stateless Session Bean erstellt, die in regelmäßigen Abständen gestartet wird und verschiedenen RSS-Feeds abfragt, auswertet und die wichtigsten Informationen in die Datenbank schreibt.
Mit der Annotation @Schedule(minute = "*/15", hour="*")
sorgt man dafür, dass die Methode pullRssMessages()
alle 15 Minuten gestartet wird.
Hier noch ein paar Beispiele für verschiedene Startzeitpunkte:
Zeitangabe | Annotation |
---|---|
Jeden Donnerstag | @Schedule(dayOfWeek="Thu") |
Jeden Dienstag um Mitternacht | @Schedule(dayOfWeek="Tue", second="0", minute="0", hour="0", dayOfMonth="*", month="*", year="*") |
Jeden Montag und Freitag | @Schedule(dayOfWeek="Mon, Fri") |
Alle 10 Minuten von 8-9 und von 12-13 Uhr | @Schedule(minute="*/10", hour="8,12") |
Eine ausführliche Beschreibung zu den zahlreichen Möglichkeiten eine Methode zu starten findet man im JEE6-Tutorial von Oracle im Kapitel Using the Timer Service.
package org.hameister.rss; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.text.DateFormatSymbols; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; import javax.ejb.Schedule; import javax.ejb.Stateless; import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * * @author Hameister */ @Stateless @Named public class RssReaderBean { @PersistenceContext EntityManager em; @Schedule(minute = "*/15", hour="*") public void pullRssMessages() throws MalformedURLException, IOException, ParserConfigurationException, SAXException, ParseException, XPathExpressionException { List<URL> urls = Arrays.asList( new URL("http://www.hameister.org/Blog/?feed=rss2"), new URL("http://www.spiegel.de/schlagzeilen/tops/index.rss") ); for(URL url : urls) { readFeed(url); } } private String getElementValue(String elementName, Element feedElement) { NodeList nodeList = feedElement.getElementsByTagName(elementName).item(0).getChildNodes(); return ((Node) nodeList.item(0)).getNodeValue(); } private void readFeed(URL url) throws IOException, ParserConfigurationException, SAXException, ParseException, XPathExpressionException { //Open Connection and get input stream URLConnection connection = url.openConnection(); InputStream inputStream = connection.getInputStream(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); //Parse input stream into DOM Document document = builder.parse(inputStream); document.getDocumentElement().normalize(); //Read title and link with XPath XPath xpath = XPathFactory.newInstance().newXPath(); Element e = (Element) xpath.evaluate("/rss/channel", document,XPathConstants.NODE); String source = getElementValue("title", e)+" "+getElementValue("link", e); //Get all item elements NodeList items = document.getElementsByTagName("item"); for (int itemNumber = 0; itemNumber < items.getLength(); itemNumber++) { Node feedEntry = items.item(itemNumber); if (feedEntry.getNodeType() == Node.ELEMENT_NODE) { Element feedElement = (Element) feedEntry; //Wed, 31 Aug 2012 22:05:06 +0000 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", new DateFormatSymbols(Locale.US)); Date parsed = format.parse(getElementValue("pubDate", feedElement)); RSSMessage message = new RSSMessage(); message.setTitle(getElementValue("title", feedElement)); message.setLink(getElementValue("link", feedElement)); message.setMessageDate(parsed); message.setSource(source); //Merge RSSMessage into the DB em.merge(message); System.out.println("Merged:"+ message.getLink()+" "+message.getTitle()); } } inputStream.close(); } }
Zu dem Quellcode oben sind noch folgende Dinge anzumerken:
Zum Auslesen des RSS-Feeds wird eine Verbindung URLConnection
über ein URL
-Objekt geöffnet und der InputStream
mit dem Feed ausgelesen.
Der InputStream
wird in einem DOM (Document Object Model) zwischengespeichert. (Das funktioniert hier, weil die Feeds keinen großen Speicherbedarf haben.)
Anschließend wird mittels eines XPath
der title
und der link
des Feeds ausgelesen. (Man hätte auch ohne XPath
direkt auf dem DOM navigieren können.)
Mit der Methode getElementsByTagName
werden alle Nachrichten-Elemente ausgelesen und in einer NodeList
gespeichert und anschließend der Reihe nach ausgewertet. Informationen zum Aufbau eines RSS-Feeds findet man bei Wikipedia unter RSS.
Zum Parsen des Datums wird ein SimpleDateFormat
verwendet. Weitere Erklärungen dazu findet man unter SimpleDateFormat.
Stateless Session Bean zum Auslesen der RSS-Messages
In der folgenden Stateless Session Bean wird die @NamedQuery
, die wir in der Entity-Klasse RSSMessage
definiert haben, ausgeführt und das Ergebnis wird in die Liste messages
geschrieben. Damit die neuste Nachricht ganz oben steht, wird die Liste mit einem Comparator
sortiert. (Anmerkung: Man hätte die Sortierung auch von der Datenbank durchführen lassen können!)
package org.hameister.rss; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.ejb.Stateless; import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; /** * * @author Hameister */ @Stateless @Named public class RssMessageStore { @PersistenceContext EntityManager em; public List<RSSMessage> listMessages() { List<RSSMessage> messages = new ArrayList<RSSMessage>(); Query query = em.createNamedQuery("findAllRssMessages"); List<RSSMessage> resultList = query.getResultList(); for (RSSMessage entity : resultList) { messages.add(entity); } //Sort the messages array Comparator c = new Comparator<RSSMessage>() { @Override public int compare(RSSMessage t1, RSSMessage t2) { if (t1.getMessageDate().before(t2.getMessageDate())) { return 1; } if (t1.getMessageDate().after(t2.getMessageDate())) { return -1; } return 0; } }; Collections.sort(messages, c); return messages; } }
Die JSF-Seite zum Anzeigen des Nachrichten
Was jetzt noch fehlt, ist die JSF-Seite, die die Stateless Session Bean RssMessageStore
anspricht und die Nachrichten abfragt. Zur Darstellung der Werte wird die Komponente h:dataTable
verwendet.
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <h:head> <title>RSS-Messages</title> <h:outputStylesheet library="css" name="table.css" /> </h:head> <h:body> <h:form> <h:dataTable value="#{rssMessageStore.listMessages()}" var="rssmessage" styleClass="message-table" headerClass="message-table-header" rowClasses="message-table-odd-row,message-table-even-row" columnClasses="message-table-first-column,message-table-second-column, message-table-third-column" > <h:column> <f:facet name="header">Datum</f:facet> <h:outputText value="#{rssmessage.messageDate}"> <f:convertDateTime pattern="yyyy-MM-dd HH:mm:ss" timeZone="CET"/> </h:outputText> </h:column> <h:column> <f:facet name="header">Titel</f:facet> <a href="#{rssmessage.link}" target="_default">#{rssmessage.title}</a> </h:column> <h:column> <f:facet name="header">Source</f:facet> #{rssmessage.source} </h:column> </h:dataTable> </h:form> </h:body> </html>
Damit die Tabelle schön formatiert ist, ergänzen wir noch ein Cascading Style Sheet und speichern es unter dem Namen table.css
ab.
table { width:1000px } td { padding: 5px; } .message-table-first-column { width: 150px; text-align: left; padding-left: 20px; } .message-table-second-column { text-align:left; padding-left: 100px; } .message-table-third-column { width: 300px; text-align: right; padding-right: 20px; } .message-table{ border-collapse:collapse; } .message-table-header{ text-align:center; background:none repeat scroll 0 0 #baffac; border-bottom:1px solid #BBBBBB; padding:16px; } .message-table-odd-row{ text-align:center; background:none repeat scroll 0 0 #FFFFFF; border-top:1px solid #BBBBBB; } .message-table-even-row{ text-align:center; background:none repeat scroll 0 0 #97ff83; border-top:1px solid #BBBBBB; }
Wenn die JSF-Page aufgerufen wird, dann sollte eine solche Tabelle zu sehen sein. Hinweis: Es kann sein, dass man einen Augenblick warten muss, bis die RSS-Feeds zum ersten mal abgefragt werden und die Tabelle gefüllt wird. Einfach mal Refesh klicken...

Weitere Informationen
Der Timer Service ist Teil von Enterprise JavaBeans 3.1 und gehört zum JSR-318. Hier findet man das PDF mit der Spezifikation dazu.
- JEE6-Tutorial
- EJB-Tutorial
- Interceptors
- Bean-Validation
- REST-Schnittstelle
- Remote Zugriff auf EJBs mit einem Standalone-Java-Client
- Unit-Tests für EJBs mit einem Embeddable-Container