Google App Engine voor Java: een applicatie bouwen op basis van JPA

14 April 2009 13:09 Rudie Ekkelenkamp Java

Het grote nieuws van vorige week is bijna niemand ontgaan. De Google App Engine heeft nu ook ondersteuning voor Java. Dit nieuws kwam exact een jaar na het uitbrengen van de Engine op basis van Python. En nog beter nieuws is eigenlijk dat het niet alleen ondersteuning voor Java betreft, maar voor de JVM in het algemeen. Talen zoals Groovy, JRuby en Scala werken nu out-of-the-box op de Google App Engine. Door een aantal sandbox-restricties zullen echter niet alle frameworks out-of-the-box werken (zoals bijvoorbeeld Grails). Een lijst met ondersteunde talen en frameworks staat hier.

Data-opslag

Eén van de opvallende frameworks die nog niet worden ondersteund is bijvoorbeeld Hibernate. Maar de App Engine heeft wel degelijk serieuze support voor opslaan van data. Het meest prominent is de ondersteuning van JDO en JPA. Met name JPA vind ik interessant aangezien het een JEE-standaard is. In deze blog wordt als voorbeeld een gastenboekwebapplicatie gebouwd met JPA en gedeployed op de Google App Engine met behulp van de Eclipse-plugin.

Google App Engine configureren in Eclipse

De eenvoudigste manier om aan de slag te gaan met de Google App Engine, is gebruik maken van de Eclipse-plugin.
Na het installeren van de plugin kan direct aan de slag worden gegaan door de Project Wizard te gebruiken. De plugin heeft ook ondersteuning voor de Google Web Toolkit 1.6.4, maar deze wordt hier buiten beschouwing gelaten. In deze demoapplicatie wordt met de Eclipse Wizard een project gemaakt op basis van de App Engine SDK 1.2.0.

picture-1.png

Na het aanmaken van het project wordt een projectstructuur gegenereerd die standaard uitgaat van een JDO data store. Er wordt een jdoconfig.xml file gegenereerd in de src\META-INF directory. Voor JPA-ondersteuning is deze file overbodig en kan worden verwijderd. In plaats van deze file genereren we een persistence.xml file met de volgende inhoud:

<persistence xmlns="http://java.sun.com/xml/ns/persistence">
</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_1_0.xsd" version="1.0"&gt;
<persistence-unit name="transactions-optional">
<provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
<properties>
<property name="datanucleus.NontransactionalRead" value="true"></property>
<property name="datanucleus.NontransactionalWrite" value="true"></property>
<property name="datanucleus.ConnectionURL" value="appengine"></property>
        </properties>
    </persistence-unit>

Het is een standaard JPA persistence.xml file waarin een persistence unit met de naam “transactions-optional” wordt geconfigureerd, met als provider de org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider.
De Eclipse plugin zorgt er verder voor dat alle juiste jar-files beschikbaar zijn. Dus dit is de hele data store configuratie!

Gastenboek bouwen

Om nu een gastenboek-item te kunnen opslaan, introduceren we de GuestbookItem.java entity. Voor deze demo gaan we uit van 1 gastenboek zodat er geen behoefte is aan een Guestbook entity. Hierin willen we een titel, bericht, datum en het email adres van de plaatser van het bericht opslaan. De entity ziet er dan als volgt uit (getters en setters zijn verder weggelaten):

@Entity
public class GuestbookItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String title;
 
    private String message;
 
    private Date date;
 
    private String email;

Verder definiëren we een servicelaag waarin een gastenboekbericht kan worden opgeslagen en waarmee we het laatste bericht kunnen ophalen, gesorteerd op datum.

Helaas heeft de Google App Engine geen ondersteuning voor sessie-EJB’s. Dit heeft met name als nadeel dat het afhandelen van transacties etc. programmatisch moet worden afgehandeld. Alternatief is het gebruik van de Spring framework interceptors, maar aangezien ik wil focussen op JPA, zullen we de transacties zelf afhandelen in de servicelaag.

Voor de servicelaag introduceren we de GuestbookService interface die geïmplementeerd wordt door de GuestbookServiceImpl.
Aan de servicelaag geven we een instantie van de EntityManager mee zodat deze kan worden hergebruikt. Dat is direct een nadeel van het ontbreken van sessie-EJB’s: het managen van de entity manager moeten we zelf afhandelen. In een sessie-EJB zou je eenvoudig de EntityManager injecteren. We moeten er ook zelf voor zorgen dat de EntityManager wordt afgesloten aan het einde van een request.

public class GuestbookServiceImpl implements GuestbookService {
 
	EntityManager em;
 
	public GuestbookServiceImpl(EntityManager em) {
		this.em = em;
	}

Op advies van Google creëren we een singleton wrapper class EMF die de EntityManagerFactory cached.

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");
 
    private EMF() {}
 
    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

In de constructor van de GuestbookServiceImpl kan een instantie van de EntityManager worden doorgegeven. Dit kan bijvoorbeeld vanuit een servlet worden gedaan. De EntityManager wordt dan met behulp van de EMF geïnstantieerd zoals hierna:

            EntityManager em = EMF.get().createEntityManager();
            GuestbookService service = new GuestbookServiceImpl(em);

Verder voegen we een business methode toe aan de service bean om een gastenboekbericht toe te voegen. In deze servicemethode wordt expliciet een transactie gestart en gecommit om het bericht op te slaan:

	public void addGuestbookItem(String title, String message,
			Date messageDate, String email) {
// Omdat de Google App Engine geen Session EJBs ondersteunt waarmee je
// transacties kunt managen,
// starten we de transactie programmatisch:
		try {
			em.getTransaction().begin();
			GuestbookItem item = new GuestbookItem();
			item.setTitle(title);
			item.setMessage(message);
			item.setDate(messageDate);
			item.setEmail(email);
			em.persist(item);
 
		} finally {
			em.getTransaction().commit();
 
		}
 
	}

Met behulp van een eenvoudige html-pagina maken we een tweetal invoervelden voor de titel en het bericht en posten we naar een servlet die deze service aanroept.

In de GuestbookServlet lezen we de geposte request parameters uit, zetten de datum en bepalen het email adres van de gebruiker die het bericht heeft gepost. Deze laatste optie is direct een aardig voorbeeld van het gebruik van een Google service, waarover later meer.

    /**
     * In the post voegen we een nieuw gastenboek element toe.
     */
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        EntityManager em = null;
        try {
            em = EMF.get().createEntityManager();
            GuestbookService service = new GuestbookServiceImpl(em);
            String title = req.getParameter("title");
            String message = req.getParameter("message");
            Date messageDate = new Date();
            // In de web.xml is geconfigureerd dat je ingelogd moet zijn voor
            // het posten in het gastenboek.
            // Daarom is het email adres beschikbaar als de user principal in
            // het request
            String email = req.getUserPrincipal().getName();
            // voeg de geposte gegevens toe aan het gastenboek
            service.addGuestbookItem(title, message, messageDate, email);
            resp.sendRedirect("mygoogleguestbook");
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }

Om dit te testen, kan gebruik gemaakt worden van de Google App plugin in Eclipse waarmee een lokale Google App omgeving kan worden gestart in debug modus. Dat werkt allemaal prima.
Om te voorkomen dat het gastenboek wordt volgespammed, wordt de pagina afgeschermd met een login. Hiervoor kan direct een service van de Google App Engine worden gebruikt waarmee je kunt afdwingen dat een gebruiker is ingelogd met een GMail account (of zelfs binnen hetzelfde Google Apps domein).
Door de volgende toevoeging aan de web.xml, komt de Google App Engine met een login scherm als je nog niet bent ingelogd bij GMail:

	<security-constraint>
		<web-resource-collection>
			<url-pattern>/mygoogleguestbook</url-pattern>
		</web-resource-collection>
		<auth-constraint>
			<role-name>*</role-name>
		</auth-constraint>
	</security-constraint>

Zoals in voorgaande voorbeeld te zien is, kan na het inloggen het emailadres van de ingelogde gebruiker worden verkregen van het request:
req.getUserPrincipal().getName();

Naast het opslaan van een bericht, is het ook aardig om de reeds geplaatste berichten te laten zien. De service laag wordt daartoe uitgebreid met een businessmethode die de laatste 10 berichten toont. Door middel van een eenvoudige JPA-query is dit snel geschreven:

	public List<guestbookitem> getLatestGuestbookItems() {
		List<guestbookitem> result = new ArrayList<guestbookitem>();
		Query q = em
		.createQuery("select g from GuestbookItem g order by date desc");
		q.setMaxResults(10);
		List<guestbookitem> results = q.getResultList();
		if (results == null) {
			return result;
		} else {
			return results;
		}
 
	}
</guestbookitem></guestbookitem></guestbookitem></guestbookitem>

Helaas bleek deze query niet te werken. Het GuestbookItem bleek niet gevonden te kunnen worden. De App Engine geeft dan de volgende exceptie (hoewel er wel net een bericht is opgeslagen).

GuestbookItem
org.datanucleus.exceptions.ClassNotResolvedException: GuestbookItem
at org.datanucleus.util.Imports.resolveClassDeclaration(Imports.java:194)
at org.datanucleus.query.compiler.JavaQueryCompiler.compileFrom(JavaQueryCompiler.java:209)

Dit lijkt nog een bug te zijn (of ik heb nog iets over het hoofd gezien), maar expliciet toevoegen van de entity aan de persistence.xml file aan de classes sectie gaf ook geen resultaat. Wat wel werkt, is het opgeven van de package naam in de JPA query.

.createQuery(“select g from com.ekkelenkamp.GuestbookItem g order by date desc”);

Na deze aanpassing blijkt ook deze query goed te werken.

In de doGet methode van de servlet schrijven we in de response nu de berichten. Disclaimer: dit is niet volgens het MVC design principe, maar voor de demonstratie afdoende. Voor een echte applicatie is het gebruik van een web framework uiteraard een must.

    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        EntityManager em = null;
        try {
            em = EMF.get().createEntityManager();
            GuestbookService service = new GuestbookServiceImpl(em);
            List<guestbookitem> result = service.getLatestGuestbookItems();
            resp.setContentType("text/plain");
            resp.getWriter().println("Rudie's google apps gastenboeknn");
            for (GuestbookItem e: result) {
                resp.getWriter().println("nTitel: " + e.getTitle());
                resp.getWriter().println("Bericht: " + e.getMessage());
                resp.getWriter().println("Datum: " + e.getDate());
                resp.getWriter().println("E-mail adres: " + e.getEmail());
            }
        } finally {
            if (em != null) {
                em.close();
            }
        }
    }
</guestbookitem>

Deployen van de applicatie

Nu de applicatie klaar is om te deployen, moeten er nog een aantal zaken geregeld worden.
Ten eerste zal er een applicatie geregistreerd moeten worden bij de App Engine. Dit regel je allemaal in de admin console van de App Engine. Geregistreerde applicaties zijn hier te vinden. Als je beschikt over een Google Apps domein dan kun je ze vinden op: http://appengine.google.com/a/your-domain. Hier registreer je een application identifier. In het onderstaande voorbeeld is dat “guestbook-rudie�?.
picture-3.png
Als deze identifier goed is aangemaakt, moet deze worden opgenomen in de appengine-web.xml

 
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
	<application>guestbook-rudie</application>
	<version>1</version>
 
	<!-- Configure java.util.logging -->
	<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"></property>
	</system-properties>
 
</appengine-web-app>

In feite is de applicatie nu klaar om te deployen. In de Eclipse plugin zit standaard een knop om dit uit te voeren. Er wordt een keer gevraagd om met je gmail account in te loggen. Zodra de deployment is uitgevoerd, is de applicatie te bereiken op:
http://guestbook-rudie.appspot.com/

Beheer van de applicatie

Beheer van de applicatie kan door middel van het dashboard dat je voor elke applicatie krijgt. Dit ziet er als volgt uit:

picture-4.png
Op dit dashboard krijg je een uitgebreid overzicht van de resources die de applicatie gebruikt, kun je logs bekijken en heb je ook een dataviewer waarmee je beheer op de data kunt doen. Ook kun je verschillende versies van een applicatie deployen en actief maken (zodat je snel terug kunt naar een oude versie). Tijdens het deployen specificeer je in de appengine-web.xml de versie van de applicatie die je uitrolt.
Ook biedt deze viewer de mogelijkheid om GQL queries uit te voeren (wat erg veel lijkt op de query language van JPA). Zie hieronder een voorbeeld van de viewer.
picture-5.png

Conclusie

De Google App Engine lijkt JPA goed te ondersteunen en met de ondersteuning in Eclipse is het eenvoudig om applicaties te deployen. De dashboard-functionaliteit geeft je veel mogelijkheden om een applicatie te beheren. Belangrijkste uitdaging is de ondersteuning van populaire Java-frameworks zoals Hibernate en Grails.

Reageer

RSS feed for comments on this post · TrackBack URI