EJB TimerService mit JEE6 - Java Enterprise Edition 6

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 RSSMessages 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:

ZeitangabeAnnotation
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...

Create New Project

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.