Donnerstag, 23. April 2009

Google App Engine (Java) als Web-Hoster für statische Seiten


Überblick

Neben Webanwendungen ist es auch möglich die Google App Engine als klassischen Webhoster zu verwenden.

Folgende Dinge müssen dabei beachtet werden, wenn das ganze kostenlos sein soll:
  • Verfügbarer Speicherplatz: 150 MB laut Wikipedia
  • Maximale Dateigröße: 10 MB (sonst schlägt der Upload fehl)
  • Zugriffszeiten (Ping): 45 ms (Vergleich: thobach.de bei domainfactory hat 56 ms)
  • ab 25. Mai wird der Traffic von 10 GB auf 1 GB reduziert

Performance der Google App Engine bei statischen Inhalten (Performance Tests)

Performance in der Google App Engine

Im Vergleich hier die Zugriffszeiten für eine 291 Byte große Datei, die sowohl in der Google App Engine als auch auf thobach.de gespeichert wurde. Im Durchschnitt (10 Messungen) wurden bei Google 219 ms benötigt (~ 200ms Latenzzeit und ~ 19 ms für den Download) und bei domainfactory mit meiner Domain thobach.de 206 ms (~186ms Latenzzeit und ~20 ms für den Download).


Performance auf thobach.de (gehostet bei domainfactory)

Auch bei größeren Dateien gibt es kaum Probleme. Ein 8,4 MB großes Bild benötigte über Google 9,9 s (581ms Latenzzeit) und über domainfactory 9,4 s (145ms Latenzzeit). Allein die Latenzzeiten sind hier bei Google vier mal höher als bei domainfactory.

Die Performance scheint also in keinem Fall schlecht zu sein und für das Hosting gut geeignet.

Hinweis: Alle Messungen wurden über eine 8000er DSL-Leitung (Anbieter: Alice) vorgenommen.


Wie bekomme ich nun meine statische Webseite in die Google App Engine?

  1. Google App Engine Account erstellen und die Java Funktion freischalten lassen.
  2. Eclipse installieren und die Google Plugins integrieren.
  3. Über das Google App Engine Dashboard eine neue Anwendung anlegen (die Seite wird dann unter http://application-id.appspot.com/ verfügbar sein, wobei die "application-id" frei zu wählen ist).
  4. "Web Application Project" in Eclipse erstellen.
  5. Alle benötigten statischen Dateien im Order "war" ablegen.
  6. Projekt in die Google App Engine "deployen" (rechte Maustaste auf das Projekt, Google -> Deploy to App Engine) - hier muss beim ersten Upload noch die "application-id" eingegeben werden - und Ergebnis unter http://application-id.appspot.com/ anschauen.
Damit das ganze auch noch unter der eigenen Domain funktioniert, muss man sich für Google Apps registieren und die entsprechenden DNS-Einträge auf die Google App Engine umbiegen. Ich selbst habe Google Apps in der Standard-Version und kann somit kostenlos Google Mail, Google Docs, Google App Engine etc. mit meiner Domain nutzen. So kann ich auf meine Anwendung in der Google Cloud sowohl über http://cocktailberater2.appspot.com als auch über http://cocktailberater2.thobach.de zugreifen.

Mittwoch, 22. April 2009

Google App Engine Java Tutorial (einfache CRUD Anwendung)



In diesem Tutorial werde ich zeigen, wie man eine einfache CRUD (Create, Update, Delete) Anwendung für die Google App Engine schreibt. Dabei wird Googles Implementierung des JDO Datastore benutzt.

Die Dokumentation des JDO Datastore ist leider nicht für die Erstellung einer CRUD Anwendung geeignet. Daher nun hier meine Version.

Wir werden eine Fachklasse erzeugen, die ein Attribut hat. Die Objekte dieser Fachklasse werden in der Google Cloud gespeichert (via JDO), ausgelesen, bearbeitet und können dann auch wieder gelöscht werden. Die Ausgabe aller gespeicherten Objekte soll in einer HTML-Liste geschehen und die Bearbeitung der Objekte wird über ein HTML-Formular realisiert (siehe Abbildung oben).

1. "Web Application Project" in Eclipse erstellen
Hier kann die Google Dokumentation verwendet werden. Allerdings verwenden wir als Projektnamen "CRUD" und als Packetnamen "thobach". Das Google Web Toolkit benötigen wir nicht.


2. Singleton für PersistenceManagerFactory erstellen
In dem Package "thobach" erzeugen wir eine Java-Klasse mit dem Namen "PMF" und folgendem Inhalt:

package thobach;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
private static final PersistenceManagerFactory pmfInstance =
JDOHelper.getPersistenceManagerFactory("transactions-optional");

private PMF() {}

public static PersistenceManagerFactory get() {
return pmfInstance;
}
}

Diese Klasse sichert uns einen schnellen Zugriff auf die PersistenceManagerFactory, die sonst bei ständiger Neuerzeugung viel Zeit benötigen würde.

3. Erzeugung der Fachklasse "Cocktail"
Im Package "thobach" erzeugen wir eine weitere Java-Klasse mit dem Namen "Cocktail". Diese wird unsere Business-Logik enthalten (anzeigen, speichern, bearbeiten, löschen) und hat folgenden Inhalt:

package thobach;

import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.annotations.*;

import thobach.PMF;


@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Cocktail {

@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private String name;

@NotPersistent
private static PersistenceManager pm;

public Long getId() {
return id;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

/**
* Creates a new Cocktail object with the given name
*
* @param name
* String with the name of the Cocktail
*/
public Cocktail(String name) {
this.setName(name);
}

/**
* Returns the PersistenceManager for the Cocktail Class, creates one if not
* existent or closed
*
* @return PersistenceManager
*/
private static PersistenceManager getPersistenceManager() {
if (pm == null) {
pm = PMF.get().getPersistenceManager();
} else if (pm.isClosed()) {
pm = PMF.get().getPersistenceManager();
}
return pm;
}

/**
* Finds the Cocktail in the database by the given id
*
* @param id
* integer with the id of the Cocktail
* @return Cocktail object found in the database
*/
public static Cocktail find(int id) {
Cocktail cocktail = Cocktail.getPersistenceManager().getObjectById(
Cocktail.class, id);
return cocktail;
}

/**
* Makes the Cocktail object persistent
*
* @return boolean true if successful, false if not
*/
public boolean persist() {
boolean wasSuccessful;
try {
getPersistenceManager().makePersistent(this);
wasSuccessful = true;
} catch (Exception e) {
wasSuccessful = false;
} finally {
getPersistenceManager().close();
}
return wasSuccessful;
}

/**
* Deletes the Cocktail object from the persistent storage
*
* @return
*/
public boolean delete() {
boolean wasSuccessful;
try {
getPersistenceManager().deletePersistent(this);
wasSuccessful = true;
} catch (Exception e) {
wasSuccessful = false;
} finally {
getPersistenceManager().close();
}
return wasSuccessful;
}

/**
* Returns all Cocktails as a List from the persistent storage
*
* @return
*/
@SuppressWarnings("unchecked")
public static List findAll() {
String sqlFetchAll = "select from " + Cocktail.class.getName();
Query query = Cocktail.getPersistenceManager().newQuery(sqlFetchAll);
List cocktails = (List) query.execute();
return cocktails;
}

}
Wichtig ist hier, dass die Klasse "Cocktail" ihren eigenen PersistenceManager hat. Somit müssen die Cocktail-Objekte wenn sie aus der Goolge Cloud kommen nicht "detached" werden.

4. View-Klassen: CocktailList, CocktailForm und WebPage
Getreu dem MVC-Pattern (Model-View-Controller) erstellen wir im Folgenden noch drei View-Klassen.

Die Klasse CocktailList bekommt eine Liste mit Cocktails übergeben, die sie als HTML-Liste darstellen soll. Sie hat folgenden Inhalt:

package thobach;

import java.util.List;

import thobach.Cocktail;

public class CocktailList {

private List<Cocktail> cocktails;

public CocktailList(List<Cocktail> cocktails) {
this.cocktails = cocktails;
}

public String display() {
String output = "<h2>Exisiting cocktails:</h2>" + "<ul>";
for (Cocktail c : cocktails) {
output += "<li>" + c.getName() + " <a href=\"?id=" + c.getId()
+ "&amp;action=edit\">[edit]</a> <a href=\"?id="
+ c.getId() + "&amp;action=delete\">[delete]</a></li>";
}
output += "</ul>";
return output;
}

}


Die Klasse CocktailForm stellt ein Formular zum Hinzufügen und Bearbeiten von Cocktails dar und hat folgenden Inhalt:

package thobach;

import thobach.Cocktail;

public class CocktailForm {

private Cocktail editCocktail = null;

public CocktailForm(Cocktail editCocktail) {
if (editCocktail != null) {
this.editCocktail = editCocktail;
}
}

public String display() {
String output = "<form action=\"/crud\" method=\"post\">"
+ "<fieldset><legend>Cocktail</legend>"
+ "<label for=\"name\">Cocktail name:</label> "
+ "<input type=\"text\" name=\"name\" id=\"name\" value=\""
+ (editCocktail != null ? editCocktail.getName() : "")
+ "\" />"
+ (editCocktail != null ? "<input type=\"hidden\" name=\"id\" value=\""
+ editCocktail.getId() + "\" />"
: "")
+ (editCocktail == null ? "<input type=\"hidden\" name=\"action\" value=\"add\" />"
: "")
+ (editCocktail != null ? "<input type=\"hidden\" name=\"action\" value=\"edit\" />"
: "") + "<input type=\"submit\" value=\""
+ (editCocktail != null ? "edit" : "add") + " cocktail\" /> "
+ "<input type=\"submit\" name=\"action\" value=\"reset\" />"
+ "</fieldset>" + "</form>";
return output;
}

}

Um alles zusammen zu bauen, brauchen wir noch einen Container für die Liste und das Formular - unsere eigentliche Webseite mit folgendem Inhalt:

package thobach;

public class WebPage {

private String foot;
private String head;
private String content = "";

public WebPage(String title) {
head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">"
+ "<html xmlns=\"http://www.w3.org/1999/xhtml\">" + "<head>"
+ "<title>" + title + "</title>" + "</head>" + "<body>";
foot = "</body>" + "</html>";
}

public String display() {
String output = head + content + foot;
return output;
}

public void addContent(String content) {
this.content += content;
}

}
5. Modifikation unserer Servlet-Klasse
Getreu dem MVC-Pattern stellt die Servlet-Klasse den Controller dar. Die Methoden doGet() und doPost() werden überschrieben. doPost() wird dabei die Formularbehandlung übernehmen und doGet() die Listendarstellung. In der init() Methode werden alle HTTP-Parameter abgefragt, displayContent() kümmert sich um die Ausgabe der View-Objekte.

package thobach;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import thobach.Cocktail;
import thobach.CocktailForm;
import thobach.CocktailList;
import thobach.WebPage;

@SuppressWarnings("serial")
public class CRUDServlet extends HttpServlet {

private String action;
private int id;
private String name;
private HttpServletRequest req;
private HttpServletResponse resp;

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {

init(req, resp);

// delete cocktail by id
if (action.equals("delete") && id > 0) {
Cocktail deleteCocktail = Cocktail.find(id);
deleteCocktail.delete();
id = 0;
}

displayContent();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

init(req, resp);

// add a new cocktail
if (action.equals("add") && !name.equals("")) {
Cocktail cocktail = new Cocktail(name);
cocktail.persist();
}

// edit cocktail by id - set name
if (action.equals("edit") && id > 0 && name != null && !name.equals("")) {
Cocktail editCocktail = Cocktail.find(id);
editCocktail.setName(name);
editCocktail.persist();
editCocktail = null;
id = 0;
}

displayContent();
}

/**
* Sets the content type and inits all given parameters
*
* @param _req
* @param _resp
* @throws IOException
*/
private void init(HttpServletRequest _req, HttpServletResponse _resp)
throws IOException {

req = _req;
resp = _resp;

resp.setContentType("text/xml");

// set name
name = "";
if (req.getParameter("name") != null
&& !req.getParameter("name").equals("")) {
name = req.getParameter("name");
}

// set id
id = 0;
if (req.getParameter("id") != null) {
id = Integer.parseInt(req.getParameter("id"));
}

// set action
action = "";
if (req.getParameter("action") != null
&& !req.getParameter("action").equals("")) {
action = req.getParameter("action");
}
}

private void displayContent() throws IOException {
// create XHTML page
WebPage page = new WebPage("Cocktail Database");
page.addContent("<h1>Cocktail Database</h1>");
// add list with cocktails
List<Cocktail> cocktails = Cocktail.findAll();
CocktailList list = new CocktailList(cocktails);
page.addContent(list.display());
// add form to add and edit cocktails
Cocktail editCocktail = null;
if (id > 0) {
editCocktail = Cocktail.find(id);
}
CocktailForm form = new CocktailForm(editCocktail);
page.addContent(form.display());
// print generated XHTML page
PrintWriter out = resp.getWriter();
out.println(page.display());
}
}

Nach dem Anlegen der Klassen müsste das Projekte folgende Struktur haben:

Das wars auch schon. Um das Projekt zu testen, wähle Run -> Run as ... -> Web Application und öffne im Browser die URL http://localhost:8080/.

Der komplette Code kann auch bei Google Code heruntergeladen werden.

Fragen und Kommentare sind gern willkommen. Viel Spaß!

Samstag, 11. April 2009

Erste Erfahrungen mit Google App Engine Java


Google bietet nun neben Python auch Java für "App Engine" an. Google "App Engine" ist eine Hosting-Plattform für Webanwendungen ("Platform as a Service") und zur Zeit kostenlos bis zu einer gewissen Last verfügbar.
Um sich zu registrieren benötigt man einen Google Account. Ich habe mich gleich mit meinem Google Apps Account registriert. Nach der Verifizierung meiner Handynummer per SMS-Code habe ich die Java-Funktionalität beantragt. Diese ist wohl momentan auf 10.000 Benutzer limitiert. Nach 1-2 Stunden wurde mein Java-Account freigeschaltet.

In der Zwischenzeit habe ich die Google Plugins in mein Eclipse geladen und ein erstes Demo-Projekt erstellt. Dazu habe ich ein Java Servlet geschrieben, welches meine Cocktailberater-API nach allen verfügbaren Rezepten abfragt. Meine API liefert eine XML-Datei. Diese habe ich geparst und als Liste ausgegeben.

Auf den ersten Eindruck bin ich sehr zufrieden und überrascht, wie schnell man doch hier eine Anwendung entwickeln kann. Das lokale Testen ist auch möglich, inkl. Debugging während die Anwendung lokal läuft.

Leider kann ich mich nicht immer in das App Engine Dashboard einloggen (Server Error 500). Aber nach ein paar Minuten geht es dann immer wieder.