Donnerstag Jul 29, 2010

Unable to locate Spring NamespaceHandler for XML schema namespace

Die Ursache folgender Fehlermeldung:

Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/tx]
Offending resource: class path resource [applicationContext.xml]
liegt sehr wahrscheinlich in einem Namenskonflikt während des Maven-Build-Prozesses. Genau um diese Problematik kümmert sich das maven-shade-plugin, das sich wie folgt in den Build-Prozess einbinden lässt:

<build>

<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.3.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

...
</plugins>
</build>


Anschließen sollte der Namenskonflickt beim aufruf von
mvn package
Behoben sein.

Mittwoch Apr 07, 2010

JSF mit Facelets

Auf der Suche nach einem schnellen Einstieg in das Thema Facelets bin ich auf ein sehr gutes englischsprachiges Tutorial von Richard Hightower gestoßen.

Mein Urteil: sehr zu empfehlen!

Donnerstag Mrz 11, 2010

ICEFaces Dependencies (JAR's) mit Maven konfigurieren

Wer der JSF-Framework ICEfaces verwendet wird am Anfang möglicherweise darüber stolpern, dass man zunächst nicht weiß, welche JAR's man überhaupt benötigt. ICEfaces hat hier eine Übersicht ins Netz gestellt. Für das Timezone-Tutorial habe ich mir für Tomcat 6 folgende POM (Maven) erstellt:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.neurox.tutorials</groupId>
<artifactId>timezone2</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>timezone2 Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>sun-jaxws</groupId>
<artifactId>FastInfoset</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.icefaces</groupId>
<artifactId>icefaces-comps</artifactId>
<version>1.8.2</version>
<exclusions>
<exclusion>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.icefaces</groupId>
<artifactId>icefaces-facelets</artifactId>
<version>1.8.2</version>
<exclusions>
<exclusion>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.myfaces.core</groupId>
<artifactId>myfaces-api</artifactId>
<version>1.2.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.myfaces.core</groupId>
<artifactId>myfaces-impl</artifactId>
<version>1.2.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-el</groupId>
<artifactId>commons-el</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.5</version>
</dependency>

<!--
Only required when using Excel format export with ice:dataExporter
Component
-->
<dependency>
<groupId>net.sourceforge.jexcelapi</groupId>
<artifactId>jxl</artifactId>
<version>2.6</version>
</dependency>

<!-- Only required when using the ice:outputChart component -->
<dependency>
<groupId>net.sf.jcharts</groupId>
<artifactId>krysalis-jCharts</artifactId>
<version>1.0.0-alpha-1</version>
</dependency>
</dependencies>
<build>
<finalName>timezone1</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

Leider habe ich auch eine Weile gebraucht um die passende web.xml zusammen zu stellen. Hier eine funktionierede Version in Kombination mit myfaces:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

<display-name>ICEfaces Tutorial: Timezone Part 2</display-name>

<description>
ICEfaces Tutorial: Timezone Part 2
Show how simple it is to
integrate ICEfaces technology into an existing
JavaServer Faces
environment.
</description>

<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>

<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.jspx</param-value>
</context-param>

<!--
Specifies to the ICEfaces framework that synchronous update mode is to
be used. By default, ICEfaces uses asynchronous update mode to support
server-initiated updates (AJAX push). Setting to true will enable
synchronous update mode and disable AJAX push features.
-->
<context-param>
<param-name>com.icesoft.faces.synchronousUpdate</param-name>
<param-value>true</param-value>
</context-param>

<!--
ConfigureListener is not generally required. Due to an apparent bug in
Tomcat users have reported seeing the following error "SEVERE:
ICEfaces could not initialize JavaServer Faces. Please check that the
JSF .jar files are installed correctly.". Specifying the
ConfigureListener resolves the issue.
-->
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>

<!-- Faces Servlet -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<!--
<servlet-class>org.apache.myfaces.webapp.MyFacesServlet</servlet-class>
-->
<load-on-startup>1</load-on-startup>
</servlet>

<!-- Persistent Faces Servlet -->
<servlet>
<servlet-name>Persistent Faces Servlet</servlet-name>
<servlet-class>com.icesoft.faces.webapp.xmlhttp.PersistentFacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<!-- Blocking Servlet -->
<servlet>
<servlet-name>Blocking Servlet</servlet-name>
<servlet-class>com.icesoft.faces.webapp.xmlhttp.BlockingServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<!-- Persistent Faces Servlet Mappings -->
<servlet-mapping>
<servlet-name>Persistent Faces Servlet</servlet-name>
<url-pattern>/xmlhttp/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>Persistent Faces Servlet</servlet-name>
<url-pattern>*.iface</url-pattern>
</servlet-mapping>

<!-- Blocking Servlet Mapping -->
<servlet-mapping>
<servlet-name>Blocking Servlet</servlet-name>
<url-pattern>/block/*</url-pattern>
</servlet-mapping>

<!-- Faces Servlet Mapping -->
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>

<session-config>
<session-timeout>30</session-timeout>
</session-config>

<!-- Welcome File List -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

Ich hoffe, das macht den Einstieg ein wenig leichter.

Montag Feb 15, 2010

Flash clickTAG Tutorial

Wenn man sich als Programmierer, der eine Entwicklungsumgebung wie Eclipse gewöhnt ist, in die Verlegenheit gerät mit Flash zu arbeiten, dann kann man dabei schon mal schnell Zahnschmerzen bekommen. Heute ist wieder einer dieser Tage. Glücklicherweise habe ich hier ein Tutorial gefunden, das genau auf mein Problem eingeht: einen ClickTag in einem Flash Banner einzubaue.

Mein tiefster Dank gilt den Verfassern!

Mittwoch Feb 10, 2010

java.lang.Exception: Socket bind failed: [22] Invalid argument (Apache Tomcat unter Ubuntu Karmic Koala)

Wer unter Ubuntu Karmic Koala beim Start von Apache Tomcat folgende Fehlermeldung erhält:

Error initializing endpoint
java.lang.Exception: Socket bind failed: [22] Invalid argument
at org.apache.tomcat.util.net.AprEndpoint.init(AprEndpoint.java:612)
at org.apache.coyote.http11.Http11AprProtocol.init(Http11AprProtocol.java:107)
at org.apache.catalina.connector.Connector.initialize(Connector.java:1058)
at org.apache.catalina.core.StandardService.initialize(StandardService.java:677)
at org.apache.catalina.core.StandardServer.initialize(StandardServer.java:795)
at org.apache.catalina.startup.Catalina.load(Catalina.java:530)
at org.apache.catalina.startup.Catalina.load(Catalina.java:550)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:616)
at org.apache.catalina.startup.Bootstrap.load(Bootstrap.java:260)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:412)

Der ist sehr wahrscheinlich Opfer eines Bugs in dem Paket libtcnative-1 geworden. Das Problem läßt sich umgehen, wenn man IPv6 ausschaltet. Bei den meisten Leuten kommt es im Moment (leider) noch nicht zu Einsatz. Insofern wäre das alleine schon ein Grund, das Protokoll abzuschalten.

Das Protokoll läßt sich deaktivieren, in dem man dem Kernel beim Booten einen entsprechenden Parameter mit gibt. Hierzu muss die Grub-Konfiguration editiert werden. Ich gehe hierbei von dem Standard unter Karmic Koala aus. Hier ist Grub2 Stand der Technik.

Man editiere folgende Datei:

/etc/default/grub

Dort such man nach der Zeile:

GRUB_CMDLINE_LINUX=""

und ändere sie nach:

GRUB_CMDLINE_LINUX="ipv6.disable=1"

Anschließend führt man noch folgenden Befehl aus, um die Änderungen tatsächlich anzuwenden:

sudo update-grub

Danach das System neustarten und die Katze rennt. Wenn nicht, dann lag es an was anderem ;-)

Montag Feb 01, 2010

Apache Tomcat und Apache2 HTTP Server unter Debian Linux mit mehreren Instanzen installieren (Tutorial/Howto)

Wer unter mit Java Webanwendungen programmieren möchte, der kommt in den wenigsten Fällen um Apache Tomcat herum. Selbst wenn man nicht unbedingt Programmierer ist, kommt man schnell in die Verlegenheit Tomcat installieren zu müssen um eine javabasierende Webanwendung zu verwenden.

Tomcat ist mit seinen 6 MB ausgesprochen schlank und da es sich bei Tomcat selbst auch um eine Java-Anwendung handelt, ist er obendrein weitestgehend plattformunabhänig. Dadurch braucht man Tomcat eigentlich nicht wirklich zu installieren. Man muss sich lediglich die Distribution herunterladen, entpacken und starten. Das Tomcat-Verzeichnis läßt sich hierbei auch beliebig samt installierter Applikationen jederzeit verschieben.

Wenn man aber mit Tomcat etwas intensiver arbeiten möchte oder ihn sogar im Produktivbetrieb einsetzen möchte, gilt es einige Dinge zu beachten.

In diesem Beitrag möchte ich meinen Focus im wesentlich auf zwei Themen legen:

Sicherheit
Es liegt in der Natur von Tomcat, Dienste im Internet bereit zustellen. Somit kann jeder darauf zugreifen und er kann auch von jedem angegriffen werden. Tomcat selbst ist relativ sicher, aber das alleine reicht nicht aus. Wenn die Webanwendung, die Tomcat ausführt nicht wirklich sicher ist, dann wird Tomcat zum Einfallstor für ungebetene Gäste.

Genau aus diesem Grunde sollten sich gerade Anfänger und auch Administratoren, die mal eben was ausprobieren möchten, gut überlegen wie Tomcat mit dem Netz verbunden wird. Ich werde im Folgenden beschreiben, wie man Tomcat unter Debian Linux mit reduzierten Benutzerrechten startet. Sollte hier ein Einbruch gelingen, ist nicht sofort der Superuser kompromittiert und man kann noch etwas Schadensbegrenzung betreiben.

Multiple Instanzen (Clustering)
Da Tomcat relativ leichtgewichtig ist, eigent er sich auch hervorragend dazu auch in mehreren Instanzen gestartet zu werden. Auf diese Art und Weise kann man eine Applikation zum einen etwas ausfallsicherer betreiben und zum anderen kann man auch die Applikationen selbst von einander etwas besser trennen. Wer jetzt sag: „Klar, kenne ich! Man muss nur die Installation kopieren, die Ports anpassen und die Instanzen starten ...“ der sollte vielleicht doch noch ein bisschen weiter lesen. Ich werde im Weiteren beschreiben, wie sich auch diese Schritte stark vereinfachen lassen.

Jeder Tomcat-Instanz schalte ich im produktiven Betrieb immer einen Apache2 HTTP Server vor. Tomcat selbst kommt zwar auch ohne den HTTP Server aus, denn er ist ja im Grunde genommen selber einer, aber in der Praxis müssen auch sehr viele statische Inhalte (Bilder etc.) ausgeliefert werden, was durch den Apache2 HTTP Server wesentlich schneller erfolgt. Auch wäre ein Loadbalancing mit mehreren Tomcat Instanzen ohne den Apache2 HTTP Server nicht ohne weiteres möglich.

… und los geht’s
Ich gehe davon aus, dass Debian Linux installiert und eine aktuelle JVM ebenfalls bereit steht und wir als root lokal auf der Maschine arbeiten. In der Praxis wird man vermutlich per SSH auf den Server gehen. Ich mute dem Leser also die Transferleistung zu, die Anleitung auf diesen Fall zu übertragen.

Wir müssen zunächst hier eine aktuelle Version von Apache Tomcat besorgen.

wget -nd -nH http://apache.mirror.digionline.de/tomcat/tomcat-6/v6.0.24/bin/apache-tomcat-6.0.24.tar.gz

Ihr könnt natürlich auch eine andere Version und einen anderen Mirror verwenden.

Mit

tar xfvz apache-tomcat-6.0.24.tar.gz

entpacken wir das Ganze, und erhalten dann das eigentlich Verzeichnis von Tomcat. Nun ist ein guter Zeitpunkt für einen kleinen Rundgang gekommen.

In dem frisch entpackten Tomcat-Verzeichnis finden wir folgende Verzeichnisse:

bin
conf
lib
LICENSE
logs
NOTICE
RELEASE-NOTES
RUNNING.txt
temp
webapps
work

Ich werde sie einfach der Reihe nach durch gehen:
In bin befinden sich im wesentlichen die Startscripte für Tomcat. Um die Startskripte verwenden zu können müssen wir sie zuvor noch mit

chmod +x bin/*.sh

ausführbar machen. Anschließend starten wir Tomcat das erste mal mit

bin/startup.sh

was nur dann Probleme bereitet, wenn einer oder mehrere der von Tomcat vernwendeten Standardports bereits in Verwendung sind.

Wenn wir also in unserem Browser http://localhost:8080 eingeben, sollten wir auf die Startseite von Tomcat kommen.

Falls das nicht funktioniert, dann sollten wir zunächst prüfen, ob die Ports 8005, 8009 und 8080 schon durch irgendeine andere Applikation verwendet werden. Das ist gar nicht so unwahrscheinlich, denn Tomcat wird in viele Applikationen eingebettet und dort oftmals auch mit diesen Ports verwendet.

Die Startseite von Tomcat ist selbst nur eine Webapplikation, die wir später beliebig ersetzen können. Der Voreilige Leser wird sicherlich schon unter dem Punkt Administration auf Status geklickt haben und an der Sicherheitsabfrage gescheitert sein. Ich werde an passender Stelle erklären, wie man diesem Problem begegnet.

Wir wenden uns wieder der Konsole zu und stoppen Tomcat mit

bin/shutdown.sh

Die Skripte bin/startup.sh und bin/shutdown.sh haben noch ihre Pendants für die Windowswelt und funktionieren analog. Sie sind eigentlich nur Wrapperscripte für bin/catalina.sh.

Wer ist eigentlich Catalina? Nun, dass ist der eigentliche Servlet-Container, der zusammen mit dem Coyote Connector Tomcat bilden. Zu dem Connector werde ich später kommen, den brauchen wir um Tomcat hinter den Apache2 HTTP Server zu hängen.

Im Verzeichnis lib befinden sich eine Reihe von Bibliotheken, die Tomcat selbst nutzt, die aber auch den Webapplikationen zur Verfügung stehen. Es befinde sich nicht besonders viel in diesem Verzeichnis, was auch so gewollt und auch gut so ist. Tomcat ist eben „nur“ ein Servlet-Container, den wir bei Bedarf erweitern können. Das bedeutet, dass wir selbst für eine stinknormale Datenbankverbindung die Entsprechenden Treiber unserer Webapplikation mitgeben müssen, oder diese auch in das lib-Verzeichnis von Tomcat legen können.

Allerdings würde ich von letzterem aus Gründen der Portabilität abraten. Irgendwann muss man dann doch mal die Applikation auf einen anderen Server schieben und wundert sich, weshalb einem eine ClassLoader Exception um die Ohren fliegt. Schöner ist es dann ja doch eine Applikation zu haben, in der schon alles drin ist. In der Regel ist der Festplattenplatz für unsere Anwendungen nicht unbedingt der Flaschenhals. Zeit, die bei der Fehlersuche drauf geht, ist hingegen kostbar.

Das Verzeichnis conf beeinhaltet, wie der Name schon vermuten lässt, die Konfiguration von Tomcat und auch teilweise von unseren Webapplikationen. Die Datei server.xml enthält wohl die wichtigsten Konfigurationen für den Betrieb von Tomcat. Ich empfehle von dieser Datei ein Backup zu machen

cp -ax server.xml server.xml.DEFAULT

und anschließend in der Datei alle Kommentare raus zuschmeißen. Am besten macht man dass mit einen XML-Editor. Was dann übrig bleibt, ist für uns am Anfang relevant. Der Rest ist im Augenblick als Dokumentation zu betrachten. In dieser Datei werden wir später auch die Ports (shutdown 8005, AJP 8009, HTTP 8080) anpassen, denn wie ich Eingangs schon erwähnt hatte, verhindert eine Mehrfachbelegung der Ports den Start von Tomcat. Wenn wir mehrere Tomcat-Instanzen gleichzeitig starten möchten, dann müssen wir zwangsläufig die Ports der einzelnen Instanzen aufeinander abstimmen, damit sie sich nicht ins Gehege kommen.

Hier finden wir auch die Datei tomcat-users.xml. Die nichtvorhandenen Einträge in dieser Datei haben uns zuvor davon abgehalten alle Bereiche unserer Manager-Application zu verwenden.

Das Wurzelement der XML-Datei ist <tomcat-users />. In der Minimalkonfiguration muss es für den Zugriff auf die Manager-Applikation mindestens zwei Kindelemente besitzen: <role /> und <user/> .

Mit <role /> wird zunächst eine Benutzergruppe definiert. Die Manager-Applikation interessiert sich z.B. gar nicht für den Benutzer, sondern nur für die Gruppe. D.h. nur Mitglieder der Gruppe manager erhalten Zugriff auf die Applikation.

<user/> Definiert den Benutzer, sein Passwort und die Gruppe(n), in denen der Benutzer Mitglied ist. Mehere Gruppen werden innerhalb des Parameters „roles“ per Kommata voneinander getrennt.

Eine funktionstüchtige tomcat-users.xml sieht wie folgt aus:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager"/>
<user username="admin" password="geheimesPasswort" roles="manager"/>
</tomcat-users>

Ich kann es gar nicht oft genug sagen, wohl wissend, dass viele mich nicht erhören werden:

Verwendet bitte einzigartige und komplexe Passwörter auch in der Testumgebung! Zu schnell werden besonders diese Konfigurationsdateien mit in die Produktivumgebung kopiert. Selbst wenn sie nur im Testbetrieb laufen und trotzdem im Internet exponiert werden, so stellen schwache Passwörter ein sehr hohes Risiko für das gesamte System dar!

Im Produktivbetrieb hat sich der Einsatz von JNDI/LDAP bewährt. Eine Erklärung würde hier aber zu weit führen.

Wir können Tomcat nun mit bin/startup.sh (ggfls. vorher mit bin/shutdown.sh runterfahren) starten und unter http://localhost:8080/ auf Tomcat Manager klicken. Nachdem wir in der Passwortabfrage unsere zuvor hinterlegten Zugangsdaten eingegeben haben, gelangen wir auf die Manager-Applikation. Hier können wir nun durch einfaches hochladen einer WAR-Datei eine Applikation auf dem Tomcat starten.

Wir haben unseren Tomcat nun also einigermaßen im Griff. Was im Augenblick stört, ist dass Tomcat noch als root ausgeführt und nicht automatisch beim Booten mit gestartet wird. Hierzu müssen wir nun einige notwendige Schritte unternehmen:

Zunächst legen wir wir

useradd tomcat
Den Benuter an, unter dem Tomcat in Zukunft ausgeführt wird. Anschließend verschieben wir das Tomcat Verzeichnis an einen beliebigen Ort, wo es dauerhaft bleiben kann. In meinem Fall habe ich mich für /usr/lib/ entschieden. Wer ein anderes Verzeichnis wählt, muss die Pfade entspreche abändern.

mv apache-tomcat-6.0.24 /usr/lib

Um bei zuküntigen Updates von Tomcat nicht immer gleich alle Pfade in den Skripten ändern zu müssen, legen wir noch einen Softlink an, der auf das aktuelle Tomcat-Verzeichnis verweist:

cd /usr/lib
ln -s apache-tomcat-6.0.24/ tomcat6

Wenn wir eine neue Tomcat-Version installieren, brauchen wir nur den Link zu ändern.

Das Verzeichnis

/usr/lib/tomcat6/

ist also nun unser endgültiges Installationsverzeichnis. Wenn in der Dokumentation von Tomcat von CATALINA_HOME die Rede ist, dann ist genau dieses Verzeichnis gemeint.

Die Dateien in CATALINA_HOME sind bei jeder Tomcat-Instanz die selben und es wird nur lesend darauf zugegriffen. Aus diesem Grunde brauchen wir dem User tomcat an dieser Stelle noch keine besonderen Rechte auf das Verzeichnis einzuräumen.

Wir benötigen nun die Verzeichnisse, in denen wir die einzelnen Tomcat-Instanzen unter dem User tomcat ausführen möchten. Bei diesem Verzeichnis handelt es sich um CATALINA_BASE. Das erste Verzeichniss erstellen wir mit

mkdir -p /svr/tomcat/1

und wechseln mit

cd /svr/tomcat/1/

in selbiges.

mit

for i in bin conf logs temp webapps work ; do cp -ax /usr/lib/tomcat6/${i} ./ ; done

kopieren wir uns die Dateien, die wir für jede einzelne Tomcat-Instanz exklusiv benötigen.

Wir legen noch mit

mkdir run

ein Unterverzeichnis an, in dem Später die PID der Instanz abgelegt wird.

Was nun folgt, ist wohl die komplexeste Änderung, die wir an der Standardinstallation von Tomcat durchführen werden. Die Datei bin/catalina.sh erweitern wir am Anfang der Datei um folgende Zeilen:

MY_PATH="`dirname \"$0\"`"
MY_PATH="`( cd \"$MY_PATH\" && pwd )`"
if [ -z "$MY_PATH" ] ; then
exit 1
fi

VAR=`echo ${MY_PATH} | awk -F / '{ print $(NF-1) }'`

test -z "${VAR}" -o -n "`echo \"$VAR\" | tr -d '[0-9]'`" && IS_NUMBER=FALSE

if [ IS_NUMBER != "FALSE" ] ; then
typeset -i VAR
CATALINA_HOME=/usr/lib/tomcat6
CATALINA_BASE=${MY_PATH%????}
let PORT_PREFIX=80+$VAR
CATALINA_OPTS="-DPORT_PREFIX=${PORT_PREFIX} -DWORKER_NO=${VAR}"
CATALINA_PID=${CATALINA_BASE}/run/tomcat.pid
JRE_HOME=/usr/lib/jvm/java-6-openjdk
fi

Bitte achtet darauf, dass ihr die Variable JRE_HOME entsprechend eurer Umgebung setzt.

Was hier passiert bedarf sicherlich einiger Erklärung:

Wie ich am Anfang des Tutorials bereits angedeutet habe, war es mir irgendwann zu lästig, mit jeder neuen Tomcat-Instanz immer die Ports hochzuzählen. So etwas wie einen PortOffset wie der Applikationsserver Apache Geronimo kennt Tomcat leider nicht. Mein Dank gilt an dieser Stelle Peter Roßbach, der mir seines Zeichens als Tomcat Entwickler den Tipp gab per -DPORT_PREFIX einen PortPrefix als Parameter an die JVM zu übergeben und diesen in der server.xml (und ggfls. context.xml und web.xml) per

<Connector port="${PORT_PREFIX}01" .... />

den Wert wieder auszulesen und Tomcat mit entsprechenden Werten zu starten. Den Prefix selbst errechne ich einfach aus dem Pfad also aus CATALINA_BASE. Das erste Verzeichnis trägt in diesem Tutorial die Nr. 1 (/svr/tomcat/1/). Wenn wir alle Änderungen abgeschlossen haben, brauchen wir einach nur noch das Verzeichnis /svr/tomcat/1/ nach /svr/tomcat/2/ und /svr/tomcat/3/ etc. zu kopieren und die Ports werden automatisch angepasst.

Noch haben wir aber ein paar Handgriffe zu tägigen. Unsere server.xml ändern wir wie folgt:

<?xml version='1.0' encoding='utf-8'?>
<Server port="${PORT_PREFIX}05" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JasperListener" />
<Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Connector port="${PORT_PREFIX}80" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="${PORT_PREFIX}09" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat${WORKER_NO}">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true"
xmlValidation="false" xmlNamespaceAware="false">
</Host>
</Engine>
</Service>
</Server>

Es werden nun die Variablen für die bestimmung der Ports verwendet und ich bin hier auch schon einen Schritt weiter gegangen und habe die jvmRoute dynamisch gesetzt. Jede Tomcat-Instanz muss einen eindeutigen Namen tragen, damit sie bei einer Anbindung an den Apache2 HTTP Server erkannt werden kann. Dieser Name ist die jvmRoute. Auch diese Name errechne ich aus dem Verzeichnisnamen von CATALINA_BASE.

chown -R tomcat:www-data /svr/tomcat/*

Macht den User tomcat zum neuen Besitzer des Verzeichnisses und gibt der Gruppe www-data ebenfalls Zugriff.

Nun möchten wir unser Debian noch dazu überreden Tomcat beim Systemstart mit den Rechten des Users tomcat auszuführen:

vi /etc/init.d/tomcat

und schreiben folgenden Inhalt:

#!/bin/sh
# Tomcat Init-Script
SU=/bin/su
TOMCAT_USER=tomcat
WORKDIR=/svr/tomcat

start() {
for var in `ls -1 /svr/tomcat/`
do
$SU $TOMCAT_USER -c "${WORKDIR}/${var}/bin/catalina.sh start"
done
}

stop() {
for var in `ls -1 /svr/tomcat/`
do
$SU $TOMCAT_USER -c "${WORKDIR}/${var}/bin/catalina.sh stop -force"
done
}

case $1 in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
esac

exit 0

Anschließend machen wir die Datei mit:

chmod +x /etc/init.d/tomcat

noch ausführbar.

Ein erster Start mit

/etc/init.d/tomcat start

sollte erfolgreich sein, so dass wir den die neue Tomcat Instanz unter http://localhost:8180 erreichen können. Wenn wir schon weitere Kopien von CATALINA_BASE erstellt haben, werden auch diese automatisch gestartet. Die HTTP-Ports sind dann 8280, 8380, 8480 usw.

Mit

/etc/init.d/tomcat stop

werden alle Tomcat Instanzen wieder abgeschossen.

Damit die Tomcat Instanzen nun automatisch mit dem Systemstart starten, legen wir nun noch die notwendigen Startdateien an:

for i in rc3.d/S90tomcat rc0.d/K10tomcat rc4.d/S90tomcat rc5.d/S90tomcat rc6.d/K10tomcat rc2.d/S90tomcat rc1.d/K10tomcat; do ln -s /etc/init.d/tomcat /etc/${i} ; done

Was unseren Tomcat angeht, so ist unsere Arbeit damit eigentlich auch schon getan. Allerdings möchten wir eigentlich nicht den Tomcat direkt als Webserver ins Internet stellen, denn so wäre ja auch gar kein Loadbalancing möglich.

Aus diesem Grunde werden wir Tomcat nun noch mit dem Apache2 HTTP Server verbinden.

Auf meinem System ist er schon installiert und ich denke die Meisten sind auch in der Lage ihn per aptitude oder apt-get zu installieren, deshalb werde ich mich jetzt auch den Teil beschränken, der sich um die Tomcat-Anbindung dreht.

Mit

apt-get install libapache2-mod-jk

installieren wir den Connector.

Anschließen sollten wir folgende Datei im Konfigurationsverzeichnis von Apache2 finden:

/etc/apache2/mods-available/jk.load

Dieses Modul müssen wir nun noch aktivieren, in dem wir einen Softlink setzen:

cd /etc/apache2/mods-enabled/ && ln -s ../mods-available/jk.load ./

Dann legen wir eine Datei namens /etc/apache2/workers.properties mit folgendem Inhalt an:

worker.list=helloWorld
worker.template.port=8009
worker.template.host=localhost
worker.template.type=ajp13
worker.template.lbfactor=1
worker.template.cachesize=10
worker.template.cache_timeout=600
worker.template.socket_keepalive=1
worker.template.socket_timeout=300
worker.tomcat1.reference=worker.template
worker.tomcat2.reference=worker.template
worker.tomcat3.reference=worker.template
worker.tomcat4.reference=worker.template
worker.tomcat5.reference=worker.template
worker.tomcat1.port=8109
worker.tomcat2.port=8209
worker.tomcat3.port=8309
worker.tomcat4.port=8409
worker.tomcat5.port=8509
worker.helloWorld.type=lb
worker.helloWorld.sticky_session=True
worker.helloWorld.balanced_workers=tomcat1,tomcat2,tomcat3,tomcat4,tomcat5

In dieser Datei haben wir zunächt ein Template von einem Worker angelegt. Anschließ referenzieren wir in den einzelnen Instanzen auf das Template und ändern nur noch das ab, worin sich die Worker voneinander unterscheiden. In diesem Fall ist das nur der Port. Diese Worker fassen wir dann wieder zu einem virtuellen Worker für Apache2 zusammen zusammen. Fällt ein Worker aus, oder ist einfach überlastet, so bemerkt der Besucher der Website nichts, da ja noch vier andere Worker vorhanden sind. Auf diese Weise kann ich die Last auch auf eine ganze Serverfarm verteilen. In dem Fall ist dann die Adresse des Workers eben nicht localhost, sondern ein entferntes System. In größeren Systemen (wie z.B. mobile.de mit ca. 500 Servern) sind mehrere hundert Worker (natürlich auf jeweils eigener Serverhardware verteilt) nichts Ungewöhnliches.

Die Properties-Datei müssen wir dem Apache2 noch bekann machen indem wir die Datei /etc/apache2/conf.d/jk.conf mit folgendem Inhalt anlegen:

<IfModule mod_jk.c>
JkWorkersFile /etc/apache2/workers.properties
JkLogFile /var/log/apache2/mod_jk.log
JkLogLevel error
</IfModule>

Damit uns für unser Beispiel auch genügend Tomcat Instanzen zur Verfügung stehen, legen wir einfach noch ein paar Kopien davon an:

cd /svr/tomcat/
for i in 2 3 4 5 ; do cp -ax 1 ${i} ; done
/etc/init.d/tomcat restart

Nun sollten fünf Tomcat Instanzen gestartet sein. Exemplarisch werden wir nun noch die mitgelieferten Beispielanwedungen hinter den Apache2 hängen. Wir editieren hierzu die Datei /etc/apache2/sites-available/default und fügen vor dem Tag </VirtualHost> folgendes ein:

### Tomcat ###
<IfModule mod_jk.c>
JkMount /examples/* helloWorld
JkLogLevel debug
</IfModule>
### Tomcat ###

Mit diesem Eintrag weisen wir Apache2 an, alles was unterhalb von examples kommt an den Tomcat Worker „helloWorld“ zu deligieren. Wenn wir nun http://localhost/examples/ eingeben, werden uns die Tomcat-Beispielanwendungen angezeigt. Unschwer können wir erkennen, dass wir hier keine exotischen Ports mehr eingeben müssen, sondern dass die Seiten, wie eine ganz normale Website über den Apache2 auf Port 80 ausgeliefert werden.

Wir sind nun in der Lage beliebig viele Tomcat Instanzen durch einfache kopieren der Verzeichnisse zu erstellen und haben diese auch erfolgreich mit Apache2 verbunden.

Wenn wir nun in unserer Webanwendung ein z.B. ein Verzeichnis mit Bildern haben, dann ist es sinnvoll, diese statischen Inhalte durch den Apache2 ausliefern zu lassen. Das Bewerkstelligen wir, in dem wir das die Dateien auf dem Apache2 verfügbar machen und per

JkUnMount /examples/graphics/* helloWorld

aus dem Seitenbaum wieder herausnehmen. So kann sich Tomcat um das generieren von Seiteninhalten kümmern und Apache2 macht das, was er am besten kann: performant statische Inhalte ausliefern!

An dieser Stelle möchte ich einen Schönheitsfehler bei der ganzen Sache nicht verschweigen: Beim herunterfahren von Tomcat wird die Datei server.xml noch ein mal geparst. Dabei werden leider nicht die Variablen für den Shutdown Port 8x05 richtig erkannt. Tomcat kann somit nicht vernünftig herunter gefahren werden. In dem Init-Skript habe ich daher eine Option eingebaut, die das Herunterfahren von Tomcat durch einen Kill-Befehl erzwingt. Das ist sicherlich nicht optimal und wen das stört, der muss dann doch in der server.xml den Port manuel eingeben. Dann funktioniert das herunterfahren wieder über die vorgesehene Methode.

Trotz allem denke ich, dass man mit dieser Vorgehensweise sehr schnell eine robuste Test- und Produktivumgebung mit Apache Tomcat und Apache2 HTTP Server aufbauen kann.

Für Kritik und Anregung habe ich natürlich immer ein offenes Ohr.

Sonntag Dez 06, 2009

Linux Shell (Bash) per cd ins vorherige Verzeichnis springen

Auch als überzeugter Konsolenbenutzer wünscht man sich manchmal einen "Zurück-Button" um einfach wieder in das vorherige Verzeichnis zu springen. Aber auch diesen Wunsch erfüllt uns die Shell in gewohnter Einfachheit.

Folgender Befehl befördert uns dahin, wo wir herkamen:

cd -

Freitag Okt 23, 2009

ruhrjug: Vortrag von Peter Doschkinow (Sun) zu JavaFX

Gestern habe ich mir auf dem Treffen der ruhrjug in Essen (danke an Heiko Sippel) den Vortrag von Peter Doschkinow (Sun) zu JavaFX angehört. Der kostenlose Vortrag fand wie immer in einer sehr lockeren Atmosphäre bei Softdrings und Sandwiches statt, die vom Linuxhotel gesponsert wurden.

JavaFX ist eine Technologie zum erstellen von grafischen Benutzeroberflächen und tritt gegen Flex (Flash) und Silverlight an.

Während Flex eher aus der Sicht eines Designers entwickelt wurde und sich bei mit der Entwicklungsumgebung in Richtung der Programmierer bewegt, verhält es sich bei JavaFX genau umgekehrt.

JavaFX stellt zunächst eine starr typisierte Skriptsprache bereit, die jedoch auf alle bestehenden Java-Objekte zugreifen und diese instanzieren kann. Das ist möglich, da JavaFX durch dir JVM ausgeführt werden kann und somit in der selben Laufzeitumgebung wir Java selbst läuft.

Für den Java Programmierer bedeutet dies, dass er seine Backendtechnologie (Webservices, Datenbankzugriffen etc.) mit JavaFX weiterverwenden kann. Somit benötigt man JavaFX wirklich nur zur reinen Oberflächengestaltung und kann alles weitere in Java programmieren.

Für JavaFX stehen Plug-Ins für die Adobe Creative Suite 3 und 4 (Illustrator und Photoshop) bereit, was für die Einbindung von Designern in den Workflow sehr gut geeignet ist.

Die Zielplatformen für JavaFX sind zum einen der Desktop, Browser, Mobiltelefone, Fernseher/Blu-ray oder Embedded Devices wie Navigationsgeräte etc..

Bei der Entwicklung verwendet man sog. Profiles, damit die Anwendung schon auf die grundsätzlichen Gegebenheiten der Zielplattform vorbereitet werden kann. So kann man in dem entsprechenden Profile z.B. auf das Adressbuch oder die Kamera des Mobiltelefons zugreifen.

Im sog. Common profile kann eine universelle Anwendung programmiert werden. Hier wird für die Schnittmenge der Zielplattformen von JavaFX programmiert. Anwendungen die für dieses Profil entwickelt wurden, können ohne Änderungen direkt auf die Zielplattformen deployt werden.

Eine Anwendung kann somit zunächst wie ein gewöhnliches Java Applet im Browser angezeigt werden. Der Benutzer kann diese Anwendung dann per Drag-and-Drop aus dem Browser heraus ziehen, den Browser schließen und trotz dem mit der Anwendung weiterarbeiten. Die Anwendung kann dabei sogar als Link auf dem Desktop abgelegt und von dort aus immer wieder gestartet werden.

JavaFX ist eine sehr junge Technologie und muss sich daher zunächst etablieren. Auf den für diese Technik prädestinierten Mobiltelefonen wie Android, Palm Pre oder iPhone sucht man JavaFX derzeit noch vergebens. Sun ist jedoch nach Angaben von Peter Doschkinow darum bemüht in Verhandlungen mit den Mobiltelefon Herstellern JavaFX auch auf diese Platformen zu bringen.

Die Zielgruppe für diese Technik sind zunächst mal die Consumer und das erste Geld wird man vermutlich mit Spielen für Handys verdienen können. Im Enterprise Umfeld wird es sicherlich so schnell keine große Rolle spielen. JSF und Co. haben da einfach die Nase vorn und JavaFX wird hier bestenfalls dadurch punkten können, dass es auf die Hardware wie Kamera etc. zugreifen kann. Ich denke dabei z.B. an Softphones im Browser (da werden die Gesetze für die Vorratsdatenspeicherung wohl wieder erweitert werden müssen ;-)).

Sony Ericsson, HTC und LG Electronics unterstützen JavaFX wohl schon. Auf dem Vortrag selbst konnten wir eine JavaFX Anwendung auf einem HTC unter Windows Mobile von Ende 2008 bewundern.

Die beste IDE für JavaFX ist lt. Peter Doschkinow wohl Netbeans. Ich werde mir davon aber noch selber ein Bild machen, denn was Eclipse (zu deutsch Sonnenfinsternis) angeht ist Sun (zu deutsch Sonne) sicherlich nicht ganz neutral.

Ob JavaFX sich durchsetzen wird, hängt sehr stark von der Community, die die Anwendungen entwickeln muss und von den Hardware Herstellern, die die entsprechenden Endgeräte produzieren müssen ab. Wenn man so will, ein Henne-Ei-Problem.

Ich persönlich habe einen sehr positiven Eindruck von JavaFX gewinnen können und werde mich auch weitergehend damit beschäftigen, da es für mich eben eine Erweiterung von Java ist und ich defacto eben nicht mit einer komplett neuen Programmiersprache anfangen muss.

Mittwoch Okt 21, 2009

Nokia (Krebber) Kundenservice (Teil 2)

Heute habe ich mein Nokia E61 Handy wieder abgeholt. Ich hatte es am Montag in der Hoffnung, dass man es evtl. noch reparieren könnte, zu der Fa. Krebber communication AG in Erkrath gebracht.

Leider war das Ergebnis ernüchternd. Man hat sich wohl sehr intensiv mit dem Gerät beschäftigt, eine Reparatur war jedoch nicht mehr möglich. Auch hatte man sich seitens Krebber bei Nokia um einen Kulanzaustausch des Gerätes bemüht, da die Garantie abgelaufen war.

Nokia schien es jedoch nicht zu interessieren, dass ohne erkennbaren Grund auf einmal das Bluetooth, eine zentrale Funktion des Gerätes, nicht mehr funktioniert. Warum auch einem Kunden, etwas auf Kulanz austauschen, wenn man dem selben Kunden doch für weitere 300 EUR (oder auch gerne mehr) wieder so ein Gerät mit haufenweise Gimmicks verkaufen kann, die er nur braucht, weil man zuvor über die Werbung einen Bedarf suggeriert hat.

Ich habe mich schon immer gefragt, weshalb man mit einem Handy Fotos machen muss? 8 Megapixel mit diesen Minilinsen? Jeder der nur ein bisschen von Fotografie versteht kann da nur den Kopf schütteln (auch wenn Carl Zeiss drauf steht). Im Internet Surfen kann ich Zuhause und wenn es ganz dringend ist auch unterwegs mit meinem Netbook, das die Hälfte kostet, ein wenig größer ist und dafür aber alles kann, was man zu surfen braucht (eine Kamera hat das im übrigen auch). Ich muss auch nicht permanent meinen Standort an Google-Maps oder sonstwen übermitteln, damit sich andere an den Datenbergen erfreuen können.

Mit dem E61 war ich offen gesagt nie wirklich zufrieden, denn es hat nie wirklich das gehalten, was die Produktspezifikationen haben erhoffen lassen. Das Voip habe ich nie zum laufen gebracht und ich denke mal, dass ich als Programmierer doch eine gewisse Gabe dazu haben sollte. Das WLAN konnte man nur gebrauchen, wenn man in unmittelbarer nähe des Access Points stand. Die Tastatur war schlecht und es hat immer Ewigkeiten gedauert, wenn man es einschalten wollte. Man hätte meinen können, dass auf dem Dingen Windows XP installiert wäre. Auch das Bluetooth ist ständig abgestürzt, bis es dann kurz nach Ablauf der Garantie ganz seinen Dienst quittiert hat.

Nun habe ich eine Bluetooth-fähige Freisprechanlage in meinem Auto und ein sündhaft teures Nokia Handy ohne Bluetooth. Was soll ich also machen?

Dabei wäre es doch so einfach gewesen, wieder die Kreditkarte zu zücken und mir bei Krebber wieder eines der schicken Handys mit Kamera und allem Pipapo zu kaufen. Der Haben-Wollen-Effekt hat mich ja schon die ganze Zeit geplagt. Ich hätte den Abend mit der neuen Bedienungsanleitung auf dem Sofa verbringen können und die ganzen vielen neuen Funktionen eines neuen Handy ausprobieren können. Zudem hätte ich noch etwas für die Wirtschaft und den Staat getan.

Ich hätte aber auch etwas für ein Unternehmen getan, das zum einen schlechte Qualität verkauft und zum anderen noch einen Vorteil daraus zieht, in dem die Kunden sofort das nächste Handy kaufen. Ein Unternehmen, dass erst Subventionen aus dem Geldbeutel deutscher Bürger bezieht um dann ins Ausland ab zuwandern.

Die Strategie mag clever sein und der finanzielle Erfolg scheint Nokia ja offensichtlich auch recht zu geben.

Mein Fazit daraus ist: Ich habe mir wieder ein Nokia Handy zugelegt. Allerdings ein altes 6310i mit Bluetooth. Das Handy hatte meine Frau noch irgendwo in der Schublade liegen und es hat/kann alles, was ich brauche. Ich kann damit telefonieren und es verbindet sich mit der Freisprechanlage meines Autos. Mehr brauche ich nicht und man möge mich davor bewahren je wieder derartig Summen für ein Handy oder irgendetwas vergleichbares aus der Mobilfunktbranche auszugeben.

Abschließend muss ich sagen, dass ich mit Krebber recht zufrieden war, auch wenn man mir dort nicht sonderlich weiterhelfen konnte. Aber als Händler/Werkstatt kann man nicht alles richten, was ein Hersteller verbockt. Auch musste ich diesmal vor Ort nicht lange warten.

Letztendlich sind es die Verbraucher, die entscheiden, ob so etwas funktioniert oder nicht. Ich habe meine Entscheidung getroffen.

Es gibt wichtiger Dinge im Leben, als Handys und Unternehmen für die Ethik und Moral zweitrangig sind.

Tut mir leid Wolfgang Schäuble, in letzter Konsequenz werde ich mein Handy vermutlich auch nur noch einschalten, wenn ich es wirklich brauche. Alleine die altersschwache Tastatur meines "neuen" Handys wird mich vor allzu intensivem Gebraucht bewahren. Damit macht die Vorratsdatenspeicherung vermutlich auch nur noch halb so viel Spaß.

Für mich ist das insgesamt das beste Geschäft. Ich habe ein Stück Freiheit durch Nichterreichbarkeit gewonnen und das Geld für das Handy und die Telefongebühren (anderes Thema) ist auf meinem Konto viel besser aufgehoben als irgendwo anders.

Samstag Okt 17, 2009

Tutorial: Hibernate mit MySQL (erste Schritte)

Die Objektorientierte Programmierung, wie man sie in Sprachen wie Java, Smaltalk und .NET kennt verträgt sich nicht wirklich mit relationalen Datenbanken. Jeder der schon einmal per JDBC Daten in einer Datenbank abgelegt hat, wird sich während der Arbeit sicherlich gefragt haben, ob man das nicht ein wenige rationeller bewerkstelligen kann. Für genau diese Problematik gibt es ORM-Framworks (Object-Relational Mapping) wie Hibernate und EclipseLink.

Nachdem ich im Rahmen eines Projektes tagelang damit beschäftigt war endlose Folgen aus SELECT und INSERT INTO zu schreiben, führte auch mich diese Frage zu Hibernate.

Hibernate ist ein Persistenzframework welches sich mit der Persistierung von Objekten in einer relationalen Datenbank beschäftigt.

Der Grundgedanke von Hibernate ist, dass sich der Programmierer im Grunde genommen nicht mehr mit den eigentlichen Datenbankabfragen beschäftigen muss. Man hat sein Objekt und führt auf der Datenbank in der Regel nur noch CRUD-Operationen (create, read, update, delete) aus. Trotzdem ist in der Praxis eine gute Kenntnis der Datenbankprogrammierung notwendig um Hibernate effektiv einsetzen zu können. In erster Linie dient Hibernate also dazu, dem Programmierer Arbeit, nicht jedoch das Denken abzunehmen.

Auch welche Datenbank verwendet wird, ist zum Zeitpunkt der Implementierung zumindest zweitrangig. Hibernate stellt für die Datenbank eine Abstraktionsschicht bereit, die mit ihren dialects eine Reihe von SQL-Datenbanken, wie MySQL, MSSQL, Postgres, HSQLDB usw. unterstützt. Die Liste ist erfreulich lang und kann hier eingesehen werden.

Der besondere Charme wird auch hier schnell deutlich, denn man gewinnt durch den Einsatz von Hibernate ein Stück Herstellerunabhängigkeit. Wer in der späteren Weiterentwicklung der Applikation gezwungen ist, die Datenbank zu wechseln, wird sich freuen, wenn er im wesentlichen nur den dialect wechseln muss und damit die Kuh vom Eis hat.

In der Praxis gestaltet sich jedoch gerade der Einstieg in Hibernate etwas holperig und deshalb möchte ich in diesem Tutorial, anhand eines Gewinnspiels in dem die Adressdaten des Teilnehmers gespeichert werden sollen, die ersten Schritte beschreiben.

Zunächst jedoch einer der wohl wichtigsten Hinweise. Ihr benötigt für das erfolgreiche Nachvollziehen des Tutorials folgende JAR-Dateien:

Beschafft euch diese bitte und legt sie in den Klassenpfad der Anwendung. Bei den Log4J-Sachen bitte auch auf die Versionsnummern achten. Ich habe ein Weilchen gebraucht, bis ich die richtige Kombination raus hatte.

An dieser Stelle möchte ich anmerken, dass sich Hibernate am besten im Zusammenhang mit Maven verwenden lässt. Wer Maven beherrscht, was wirklich nicht allzu schwer ist, kann sich sehr viel Zeit und Ärger sparen. Es gibt dazu sehr viele gute Tutorials, deshalb spare ich mir in meinem Blog die Arbeit und verweise einfach nur auf dieses Tool.

Anschließend erstellt ihr in der obersten Ebene eures Quellcodeverzeichnisses (z.B. gewinnspiel/src) eine Datei mit dem Namen log4j.properties und folgendem Inhalt:

log4j.rootLogger=DEBUG, A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A4.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

Ich habe mir für das Tutorial ein Interface für ein Objekt zur Speicherung der Personenbezogenen Daten ausgedacht. Das Interface wird dann durch eine entsprechende Klasse implementiert. Wer möchte, kann auch das Interface weglassen und direkt die zu speichernde Klasse schreiben.

Wer möchte, kann und soll auch hier schon einfach mal ein eigens Objekt verwenden, damit das ganze auch klarer wird.

Hier das Interface für mein Beispiel:

package de.buhbuhbuh.tutorial.gewinnspiel;

import java.util.Calendar;

public interface Person {

public enum Anrede { HERR, FRAU; };

public enum Land { DEUTSCHLAND {
@Override
public String toString() {
return "Deutschland";
}
}, ÖSTERREICH{
@Override
public String toString() {
return "Österreich";
}
}, SCHWEIZ{
@Override
public String toString() {
return "Schweiz";
}
}, NIEDERLANDE{
@Override
public String toString() {
return "Niederlande";
}
}, SONSTIGE{
@Override
public String toString() {
return "Sonstige";
}
};
};

public abstract long getId();

public abstract void setId(long id);

public abstract Anrede getAnrede();

public abstract void setAnrede(Anrede anrede);

public abstract String getVorname();

public abstract void setVorname(String vorname);

public abstract String getName();

public abstract void setName(String name);

public abstract String getEmail();

public abstract void setEmail(String email);

public abstract String getStrasse();

public abstract void setStrasse(String strasse);

public abstract String getHausnummer();

public abstract void setHausnummer(String hausnummer);

public abstract String getPlz();

public abstract void setPlz(String plz);

public abstract String getOrt();

public abstract void setOrt(String ort);

public abstract String getTelefonVorwahl();

public abstract void setTelefonVorwahl(String telefonVorwahl);

public abstract String getTelefonAnschlussNummer();

public abstract void setTelefonAnschlussNummer(String telefonAnschlussNummer);

public abstract Calendar getGeburtsdatum();

public abstract void setGeburtsdatum(Calendar geburtsdatum);

public abstract Land getLand();

public abstract void setLand(Land land);

public abstract Calendar getTermsAcceptedTime();

public abstract void setTermsAcceptedTime(Calendar termsAcceptedTime);

public abstract String getTermsAcceptedIp();

public abstract void setTermsAcceptedIp(String termsAcceptedIp);
}

Anschließen noch die Implementierung des Interfaces:

package de.buhbuhbuh.tutorial.gewinnspiel;

import java.util.Calendar;

public class PersonImpl implements Person {
private long id;
private Anrede anrede;
private String vorname;
private String name;
private String email;
private String strasse;
private String hausnummer;
private String plz;
private String ort;
private String telefonVorwahl;
private String telefonAnschlussNummer;
private Calendar geburtsdatum;
private Land land;
private Calendar termsAcceptedTime;
private String termsAcceptedIp;

public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getVorname() {
return vorname;
}
public void setVorname(String vorname) {
this.vorname = vorname;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getStrasse() {
return strasse;
}
public void setStrasse(String strasse) {
this.strasse = strasse;
}
public String getHausnummer() {
return hausnummer;
}
public void setHausnummer(String hausnummer) {
this.hausnummer = hausnummer;
}
public String getPlz() {
return plz;
}
public void setPlz(String plz) {
this.plz = plz;
}
public String getOrt() {
return ort;
}
public void setOrt(String ort) {
this.ort = ort;
}
public String getTelefonVorwahl() {
return telefonVorwahl;
}
public void setTelefonVorwahl(String telefonVorwahl) {
this.telefonVorwahl = telefonVorwahl;
}
public String getTelefonAnschlussNummer() {
return telefonAnschlussNummer;
}
public void setTelefonAnschlussNummer(String telefonAnschlussNummer) {
this.telefonAnschlussNummer = telefonAnschlussNummer;
}
public Calendar getGeburtsdatum() {
return geburtsdatum;
}
public void setGeburtsdatum(Calendar geburtsdatum) {
this.geburtsdatum = geburtsdatum;
}
public Anrede getAnrede() {
return anrede;
}
public void setAnrede(Anrede anrede) {
this.anrede = anrede;
}
public Land getLand() {
return land;
}
public void setLand(Land land) {
this.land = land;
}
public Calendar getTermsAcceptedTime() {
return termsAcceptedTime;
}
public void setTermsAcceptedTime(Calendar termsAcceptedTime) {
this.termsAcceptedTime = termsAcceptedTime;
}
public String getTermsAcceptedIp() {
return termsAcceptedIp;
}
public void setTermsAcceptedIp(String termsAcceptedIp) {
this.termsAcceptedIp = termsAcceptedIp;
}
}

Bis hier hin sollte es keine großen Überraschungen geben. Wir haben ein Interface (auf das wir zur Not auch verzichten können) und eine Java Bean.

Auf die Bean-Properties wird mittels Getter- und Settermethoden zugegriffen. Auch Hibernate nutzt diese Methoden. Es ist zwar auch möglich mit Hibernate Beans zu speichern, die keine Getter- und Settermethoden haben, aber das ist nicht der gängige Weg.

Als nächstes erstellen wir eine Datei mit dem Namen hibernate.cfg.xml und folgendem Inhalt:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost/gewinnspiel</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">geheim</property>
<property name="hibernate.connection.pool_size">10</property>
<property name="show_sql">true</property>
<property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- Mapping-Dateien -->
<mapping resource="de/buhbuhbuh/tutorial/gewinnspiel/personImpl.hbm.xml" />
</session-factory>
</hibernate-configuration>

In dieser Datei steuern wir das grundlegende Verhalten von Hibernate und den Zugriff auf die Datenbank.

Bitte schaut euch dringend die Ausführliche zu der Konfiguration von Hibernate hier an.

Wir geben zunächst an, welche Treiberklasse wir für den Datenbankzugriff verwenden möchten. In diesem Fall also com.mysql.jdbc.Driver. Anschließend den Pfad, den Datenbankuser und Passwort. Das Property show_sql sorgt dafür, dass der von Hibernate verwendete SQL-Code auf der Console ausgegeben wird. Besonders bei der Entwicklung ist es Sinnvoll zu sehen, was da überhaupt passiert.

Jetzt ist also ein guter Zeitpunkt, um auf unserem Server eine leere Datenbank zu erstellen.

Mit dialect Teilen wir Hibernate mit, welcher SQL-Dialekt verwendet werden soll. Im Idealfall ist das der einzige Punkt (abgesehen von dem JDBC-Treiber), den wir ändern müssen, um das Datenbankbackend auszutauschen. Wie bereits eingangs erwähnt, stehen eine Reihe von dialects für Hibernate zur Verfügung.

hibernate.hbm2ddl.auto mit update erstellt uns automatisch ein passendes Datenbankschema. So brauchen wir die Tabellen nicht per Hand anzulegen. Im Produktivbetrieb sollte man das besser auf validate setzen, um keine bösen Überraschungen zu erleben.

Zum Ende der Datei kommen wir zu den Mapping-Dateien. Hier teilen wir Hibernate mit, wo wir die Mapping-Dateien finden. In den Mapping-Dateien werden die einzelnen Klassen und ihre Felder beschrieben. Wir definieren hier, welche Properties, wie in der Datenbank abgelegt werden sollen. In der Regel legt man pro Klasse eine *.hbm.xml-Datei an und legt diese auch in dem selben Verzeichnis, wie die Klasse selbst ab. Das ist jedoch nur eine Konvention und kann im Grunde genommen nach belieben gehandhabt werden. Wichtig ist nur, dass der Pfad in der hibernate.cfg.xml angegeben wird.

Für unser Beispiel legen wir also in dem Verzeichnis de/buhbuhbuh/tutorial/gewinnspiel die Datei personImpl.hbm.xml an. Beim Mapping wird nicht das Interface, sondern die implementierende Klasse in der Mapping-Datei beschrieben. Um das zu verdeutlichen habe ich auch die Kombination aus Interface und Klasse gewählt. Hibernate interessiert sich nicht für das Interface, sondern nur für unsere Klasse.

Die Datei personImpl.hbm.xml sieht dann also so aus:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >
<hibernate-mapping>
<class name="de.buhbuhbuh.tutorial.gewinnspiel.PersonImpl">
<id name="id">
<generator class="native" />
</id>
<property name="anrede" />
<property name="vorname" />
<property name="name" />
<property name="strasse" />
<property name="hausnummer" />
<property name="plz" />
<property name="ort" />
<property name="telefonVorwahl" />
<property name="telefonAnschlussNummer" />
<property name="termsAcceptedIp" />
<property name="termsAcceptedTime" />
<property name="geburtsdatum" />
<property name="email" />
<property name="land" />
</class>
</hibernate-mapping>

Die XML-Datei hat das Wurzelelement <hibernate-mapping>. Für jede Klasse legen wir ein <class>-Element an. Pro Mapping-Datei können wir beliebig viele Klassen beschreiben. Das macht auch Sinn, wenn wir verschachtelte Objekt-Strukturen mir Arrays oder Collections haben.

Bei den gängigsten Datenstrukturen wie Primitiven, Strings oder Enums brauchen wir nicht besonders viel anzugeben. Im wesentlichen müssen wir sie nur in der Datei aufführen und Hibernate erschließt sich von selbst, wie eine Abbildung in der Datenbank aussehen muss. Das Feintuning fängt dann an, wenn man in einem String mehr als 255 Zeichen ablegen möchte. Das ist grundsätzlich kein Problem, da Hibernate aber das Datenbank-Schema anlegen muss, bevor die Instanzen der Objekte vorliegen, geht es hier von einem default-Wert von 255 Zeichen aus, den wir dann eben überschreiben müssen.

Eine Dokumentation des Hibernate O/R-Mappings findet ihr hier.

Auf eine Sache möchte ich an dieser Stelle noch eingehen, denn hier holt uns die Realität der Datenbankprogrammierung wieder ein. Um ein Objekt in der Datenbank eindeutig zu identifizieren benötigen wir einen Primärschlüssel. Selbst wenn wir diesen in unserer Geschäftslogik gar nicht brauchen, ist er für die Speicherung in der Datenbank notwendig, oder zumindest hilfreich. In unserem Beispiel habe ich die das Feld id für genau diesen Zweck vorgesehen. Primärschlüssel lässt man bei der Verwendung von MySQL üblicherweise durch die Datenbank vergeben. Wir müssen also hier deutlich machen, welches der Felder den Primärschlüssel darstellen soll. Aus diesem Grunde geben wir id, wie alle anderen Felder einfach als Property, sondern als generator-Klasse an. Des Weiteren Spezifizieren wir den Primärschlüssel noch etwas näher. Wenn wir uns an dieser Stelle schon auf MySQL festlegen wollten, dann würde es auch ein <generator class="identity" /> tun. Damit legt Hibernate eine Primärschlüsselspalte in der Tabelle an. Etwas allgemeingültiger ist jedoch <generator class="native" />. Hier werden individuellen Möglichkeiten der unterliegenden Datenbank berücksichtigt.

Auch in diesem Punkt kann ich nur noch mal auf die oben genannte Referenz zum O/R-Mapping verweisen.

Die Konfiguration von Hibernate inkl. O/R-Mapping ist damit abgeschlossen.

Einen ersten Gehversuch machen wir mit folgender Klasse:

import java.util.Calendar;
import java.util.TimeZone;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import de.buhbuhbuh.tutorial.gewinnspiel.Person;
import de.buhbuhbuh.tutorial.gewinnspiel.PersonImpl;
import de.buhbuhbuh.tutorial.gewinnspiel.Person.Anrede;
import de.buhbuhbuh.tutorial.gewinnspiel.Person.Land;

public class ErsterVersuch {

public static void main(String[] args) {
Session session = null;
try {
// Datenbankverbindung konfigurieren (hibernate.cfg.xml wird eingelesen)
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
session = sessionFactory.openSession();

System.out.println("Erstelle neuen Eintrag ...");

// Objekt instanzieren und mit Werten befüllen
Person person = new PersonImpl();
person.setAnrede(Anrede.HERR);
person.setVorname("Hans Peter");
person.setName("Mustermann");
person.setHausnummer("13");
person.setStrasse("Holzweg");
person.setPlz("00000");
person.setOrt("Musterstadt");
Calendar geburtsdatum = Calendar.getInstance(TimeZone.getTimeZone("CET"));
geburtsdatum.clear();
geburtsdatum.set(Calendar.YEAR, 1974);
geburtsdatum.set(Calendar.MONTH, Calendar.APRIL);
geburtsdatum.set(Calendar.DAY_OF_MONTH, 1);
person.setGeburtsdatum(geburtsdatum);
person.setTelefonVorwahl("0800");
person.setTelefonAnschlussNummer("1110111");
person.setTermsAcceptedIp("192.168.1.1");
person.setTermsAcceptedTime(Calendar.getInstance(TimeZone.getTimeZone("CET")));
person.setEmail("info@example.com");
person.setLand(Land.DEUTSCHLAND);

// das Objekt in der Datenbank ablegen
session.save(person);
} catch (Exception e) {
e.printStackTrace();
} finally {
session.flush();
session.close();
}
}
}

Wenn wir alles richtig gemacht haben, erscheint auf der Konsole eine sehr ausführliche Protokollierung dessen, was Hibernate gerade gemacht hat. Besonders interessant ist:

table not found: PersonImpl
create table PersonImpl (id bigint not null auto_increment, anrede tinyblob, vorname varchar(255), name varchar(255), strasse varchar(255), hausnummer varchar(255), plz varchar(255), ort varchar(255), telefonVorwahl varchar(255), telefonAnschlussNummer varchar(255), termsAcceptedIp varchar(255), termsAcceptedTime datetime, geburtsdatum datetime, email varchar(255), land tinyblob, primary key (id))
schema update complete

Wir sehen hier, wie Hibernate prüft, ob in der Datenbank eine geeignete Tabelle vorhanden ist, oder nicht. Da die Datenbank leer ist, legt Hibernate hier zunächst einmal die entsprechende Struktur an. Dieses verhalten ist dem Parameter <property name="hibernate.hbm2ddl.auto">update</property> in unserer hibernate.cfg.xml geschuldet.

Folgende Zeilen stimmen uns hoffnungsvoll, dass der Datensatz in der Datenbank gelandet ist:

executing identity-insert immediately
about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
opening JDBC connection
insert into PersonImpl (anrede, vorname, name, strasse, hausnummer, plz, ort, telefonVorwahl, telefonAnschlussNummer, termsAcceptedIp, termsAcceptedTime, geburtsdatum, email, land) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into PersonImpl (anrede, vorname, name, strasse, hausnummer, plz, ort, telefonVorwahl, telefonAnschlussNummer, termsAcceptedIp, termsAcceptedTime, geburtsdatum, email, land) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

und

de.buhbuhbuh.tutorial.gewinnspiel.PersonImpl{vorname=Hans Peter, geburtsdatum=1974-04-01 00:00:00, land=2c6d8085fef280ade4e5aee2f5e8e2f5e8e2f5e8aef4f5f4eff2e9e1ecaee7e5f7e9eeeef3f0e9e5ecaed0e5f2f3efeea4cce1eee48080808080808080928080f8f2808eeae1f6e1aeece1eee7aec5eef5ed8080808080808080928080f8f0f4808bc4c5d5d4d3c3c8ccc1cec4, telefonVorwahl=0800, hausnummer=13, id=1, strasse=Holzweg, termsAcceptedTime=2009-10-17 11:43:30, email=info@example.com, name=Mustermann, plz=00000, ort=Musterstadt, telefonAnschlussNummer=1110111, termsAcceptedIp=192.168.1.1, anrede=2c6d8085fef280afe4e5aee2f5e8e2f5e8e2f5e8aef4f5f4eff2e9e1ecaee7e5f7e9eeeef3f0e9e5ecaed0e5f2f3efeea4c1eef2e5e4e58080808080808080928080f8f2808eeae1f6e1aeece1eee7aec5eef5ed8080808080808080928080f8f0f48084c8c5d2d2}
releasing JDBC connection [ (open PreparedStatements: 0, globally: 0) (open ResultSets: 0, globally: 0)]

Die Zeile

Natively generated identity: 1

verrät uns, dass ein neuer Primäschlüssel von der Datenbank erzeugt wurde. Ein Blick in die Datenbank sollte unsere Vermutung bestätigen.

Wir sehen, dass wir eine sehr kompakte Geschäftslogik ohne Umständliche SQL-Statements und damit verbundene Fehlerquellen haben. Es entfällt einfach eine Menge Tipparbeit und wir brauchen uns z.B. auch keine Gedanken darüber zu machen, wie wir unsere ENUMs in der Datenbank ablegen. Trotzdem sind wir bei der Verwendung der Datenbank sehr flexibel, so können wir über das O/R-Mapping beliebige Tabellennamen oder Prefixe für die Tabellennamen vorgeben. Es ist auch nicht unüblich ein Reverse Engineering zu bereiben und bereits vorhandene Datenbanken per Hibernate anzusprechen. Die Hibernate-Tools unterstützen uns sogar dabei, indem sie uns den Java-Quellcode für passende Klassen aus bestehenden Datenbanken erzeugen.

Nun ist das Speichern in eine Datenbank nur die halbe Miete und so möchte ich abschließend noch eine Beispielklasse für das Lesen von Objekten aus der Datenbank vorstellen:

import java.util.Iterator;
import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import de.buhbuhbuh.tutorial.gewinnspiel.Person;

public class ZweiterVersuch {

public static void main(String[] args) {
Session session = null;
try {
// Datenbankverbindung konfigurieren (hibernate.cfg.xml wird eingelesen)
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
session = sessionFactory.openSession();

System.out.println("Lese alle PersonImpl-Objekte ...");

// Abfrage starten
Query query = session.createQuery("from PersonImpl");
List list = query.list();

// Ergebnisse anzeigen
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Person person = (Person) iterator.next();
System.out.printf("Anrede: %s\n", person.getAnrede());
System.out.printf("Vorname: %s\n", person.getVorname());
System.out.printf("Nachname: %s\n", person.getName());
System.out.printf("Straße/Hausnummer: %s %s\n", person.getStrasse(), person.getHausnummer());
System.out.printf("PLZ/Ort: %s %s\n", person.getPlz(), person.getOrt());
System.out.printf("Land: %s\n", person.getLand());
System.out.printf("Email-Adresse: %s\n\n", person.getEmail());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
session.flush();
session.close();
}
}
}

Auch hier gibt uns Log4j wieder sehr ausführlich Auskunft darüber, was passiert. Und irgendwo zwischen drin wird dann auch unser Datensatz auf der Konsole ausgegeben. In diesem Beispiel verwende ich die Abfragesprache HQL (Hibernate Query Language), die sehr an SQL erinnert. HQL ist eine sehr mächige Abfragesprache mit der wir unsere Objekte nach den verschiedensten Kriterien suchen können.

Generell gibt es sehr viele Wege um die Objekte wieder aus der Datenbank zu laden. Wir können auch natives SQL verwenden oder per Criteria Queries, auch Query By Example genannt, ein Musterobjekt erstellen und nach Objekten in der Datenbank suchen, deren Eigenschaften mit dem Musterobjekt übereinstimmen.

Wer dieses Tutorial erfolgreich nachvollziehen konnte, sollte sich im Weiteren unbedingt mit Transaktionen beschäftigen, da man eigentlich ausnahmslos Transaktionen verwenden sollte, wenn man in eine Datenbank schreibt. Ich habe das in diesem Tutorial jedoch mit Absicht ausgeklammert um den Focus auf das Nötigste zu legen. Mir geht es hier darum ein gutes Verständnis der grundlegenden Funktionen zu vermitteln.

Anzumerken sei auch noch, dass Objekte, die so einfach aufgebaut sind, wie unseres, sich auch sehr bequem per Annotation mappen lassen, dann spart man sich das explizite Mapping (siehe personImpl.hbm.xml), sondern muss nur die Klasse annotieren und ggfls. Felder markieren, die nicht in der Datenbank abgelegt werden sollen.

Für den Produktivbetrieb kann ich nur Kapselung der Datenbankzugriffe in DAOs empfehlen. Man hat am Anfang ein wenig mehr Tipparbeit, die sich aber sehr schnell auszahlt, wenn die Anwendung größer wird. An dieser Stelle sei auch das Spring Framwork genannt, dass sehr gut mit Hibernate zusammenarbeitet und auch oft in einem Athemzug mit Hibernate genannt wird.

Eine gute altenative zu ORM sei noch mit db4o genannt. db4o legt die Objekte nicht in einer Relationalen Datenbank, sondern in einer nativen Objektdatenbank ab. Hiermit entfällt jegliches Mapping und db4o reklamiert für sich, wesentlich schneller zu sein, als die abjektrelationalen Ansätze wie Hibernate und EclipseLink. Ob dem wirklich so ist, kann ich nicht sagen, da ich hier keine Tests unternommen habe. Was man allerdings ganz allgemein sagen kann, ist das die Lernkurve wesentlich steiler ist, als bei Hibernate. db4o ist extrem einfach zu verwenden, allerdings stößt man hier auch schneller an die Grenzen der Skalierbarkeit.

Freitag Okt 16, 2009

Leistungsstarker Übersetzungsdienst von Google

Ich bin gerade total begeistert von einem neuen Google-Dienst. Eine relativ unspektakuläre Meldung auf heise-Online machte mich auf diesen neuen Service aufmerksam.

Übersetzungstools gibt es ja mittlerweile wie Sand am Meer und dass sie vielfach nicht zu gebrauchen sind, weiß auch jeder, der sich damit schon einmal beschäftigt hat.

Dem Google Übersetzer kann man zwar auch nicht hundertprozentig trauen, aber die Qualität der Übersetzungen ist schon bemerkenswert. Ich habe mal spaßeshalber ein englischsprachiges Handbuch zu einer Software, das ich als PDF auf dem Desktop liegen hatte, in den Übersetzer geladen und nach ein paar Sekunden wurde mir der Text in relativ einwandfreiem deutsch angezeigt.

An einigen Stellen fanden sich zwar noch grammatikalische Fehler, die man jedoch auch ohne Kenntnis des Original-Dokumentes beheben kann.

Einen weiteren Härtetest habe ich gemacht, in dem ich chinesische Zeichen aus einer von uns programmierten Website in den Übersetzer geladen habe. Den Text hatten wir zuvor durch eine chinesische Übersetzerin übersetzen lassen. Natürlich hatten weder wir, noch unser Kunde eine Ahnung, was uns da zurück geliefert wurde. Wir mussten das ganze also in gutem Glauben auf der Website des Kunden veröffentlichen.

Wie ich jetzt festgestellt habe, sind wir von der Übersetzerin nicht über den Tisch gezogen worden und die Rückübersetzung durch Google ins deutsche hatte große Ähnlichkeit mit dem originalen Text.

Besonders für diese Sonderfälle finde ich den Dienst wirklich sehr praktisch und auch sehr gelungen. Wie es aussieht steht auch hierfür wieder eine API bereit, mit der sich der Dienst auch in eingenen Applikationen verwenden läßt.

Der Übersetzer bedient sich einer Datenbank mit vor übersetzten Textbausteinen aus 85 verschiedenen Sprachen. Ich gehe auch davon aus, dass Google sich den Vorteil zunutze macht, dass viele Website mehrsprachig sind und damit schon eine fast unerschöpfliche Quelle an Textbausteinen bereit stehen.

Extrem praktisch ist auch die Übersetzte Suche, in der man Suchbegriffe in seiner eigenen Muttersprache eingeben kann und diese in einer der verfügbaren Fremdsprachen suchen lassen kann. Die Suchergebnisse werden dann wiederum zweisprachig angezeigt. Klick man dann auf die Links, wir direkt die ganze Website übersetzt.

Ich denke mal, dass einige Barrieren für Kommunikation, Handel etc. fallen dürften, wenn man die Möglichkeiten dieses Dienstes wirklich ausschöpft.

Wie bei allen Diensten von Google muss man sich jedoch darüber im Klaren sein, dass schlicht weg alles, was man mit diesem Dienst gespeichert und verarbeitet wird. Nur so kann Google überhaupt funktionieren, ob das Datanschützern nun gefällt, oder nicht.

Donnerstag Okt 15, 2009

Nokia (Krebber) Kundenservice (Teil 1)

Ich bin seit 1994 Nokia-Kunde und hatte bisher auch nie ein Handy eines anderen Herstellers. Seit Februar 2007 habe ich nun ein E61, das leider auch nicht alles hält, was es verspricht. So kann man das Telefon für VOIP schlicht weg nicht gebrauchen und die WLAN-Fahigkeit lässt auch sehr zu wünschen übrig.

Seit kurzem funktioniert die Bluetooth-Schnittstelle des Gerätes nicht mehr, was den Funktionsumfang des Gerätes drastisch reduziert. Das Gerät hat 415 EUR gekostet und ich bin nun an einem Punkt angelangt, an dem ich sagen muss, dass ich nicht bereit bin ein Produkt für diesen Preis nach 2,5 Jahren einfach abzuschreiben.

Es scheint so, als ob Nokia der Meinung ist, das man sich nach 2 Jahren mal wieder ein neues Telefon kaufen sollte.

Leider habe ich bisher nur schlechtes über den Nokia Kundendienst gehört:

http://www.inside-handy.de/magazin/artikel187_nokia-kundenservice-mangelhaft_1.html
http://www.teltarif.de/forum/h-nokia/14-1.html
http://www.telefon-treff.de/showthread/t-100735.html
http://www.bandh.de/wp/2008/06/19/nokia-kundenservice/

Ich werde nun selber mal mein Glück mit Nokia bzw. in diesem Fall mit dem Service Partner Krebber communication AG in Erkrath versuchen. Dort hatte ich das Handy auch gekauft und ich hoffe, dass man mir dort weiter helfen kann. Ich hatte heute vormittag schon mehrfach versucht dort jemanden ans Telefon zu bekommen. Leider war bisher immer besetzt.

Am späteren Vormittag bin ich dann endlich durch gekommen. Die Dame am Telefon sagte mir, dass auf dem Gerät 2 Jahre Herstellergarantie sind und dass ich seitens Nokia mit keiner Kulanz nach Ablauf der Garantiezeit rechnen brauche. Ich sollte aber zu Ihnen ins Geschäft kommen und man würde dann schauen, was man machen kann.

Bis jetzt ist das schon fast mehr, als ich erwartet habe. Ich werde also in den nächsten Tagen nach Erkrath fahren und mich bei Krebber in das „Wartezimmer“ setzen. Leider habe ich da schon des öfteren gesessen und die Wartezeiten waren bisher immer recht lange.

Fortsetzung

Mittwoch Okt 14, 2009

for - do Schleifen in der Linux/Unix Shell (Bash)

Sich wiederholende Befehle lassen sich in der Linux Shell sehr einfach über eine for-Schleife realisieren:

for i in 1 2 3 4 5 ; do echo "Diesen Text ${i} mal anzeigen"; done

Diesen Text 1 mal anzeigen
Diesen Text 2 mal anzeigen
Diesen Text 3 mal anzeigen
Diesen Text 4 mal anzeigen
Diesen Text 5 mal anzeigen

Für jedes Element nach in erfolgt je ein Schleifendurchlauf. Bei jeden Durchlauf enthält die Variable i die Zeichenkette des gerade aktuellen Elementes. Dabei muss die Variable natürlich nicht unbedingt i heißen. Auch sind wir bei den Elementen nicht auf Zahlenwerte beschränkt. So funktioniert auch Folgendes:

for i in eins zwei drei vier fünf ; do echo "Diesen Text ${i} mal anzeigen"; done

Diesen Text eins mal anzeigen
Diesen Text zwei mal anzeigen
Diesen Text drei mal anzeigen
Diesen Text vier mal anzeigen
Diesen Text fünf mal anzeigen

Wer nicht bis fünf zählen kann oder möchte, wendet diese Variante an:

for (( i = 1 ; i <= 5; i++ )); do echo "Diesen Text ${i} mal anzeigen"; done

Ein kleiner Hinweis noch am Rande: Wer das ganze in einem Shell-Script verwenden möchte, der sollte darauf achten, dass mit der Shebang (!#) auch wirklich die Bash und nicht die Bourne Shell (sh) aufgerufen wird. Ansonsten kann so etwas schnell mit einem

Syntax error: Bad for loop variable

quittiert werden.

Die Verwendung in einem Skript würde daher so aussehen:

#! /bin/bash
for (( i = 1 ; i <= 5; i++ ))
do
echo "Diesen Text ${i} mal anzeigen"
done

Sonntag Okt 11, 2009

Gute Luft und gute Musik im Kitchen Klub Wuppertal!

Am letzten Samstag mussten wir einfach mal feiern, dass wir endlich einen Babysitter für unsere Kleine gefunden haben. Auch bedingt durch die zentrale Lage der ehemaligen Elate-Location, führte uns unser Weg in den Kitchen Klub in Wuppertal.

Die Musik, das Ambiente und das Publikum waren sehr angenehm. Besonders angenehm war auch die gute Luft, denn im Kitchen Klub rauchen die Gäste dank Stempel draußen an der frischen Luft. Es ist schön nach Hause zu kommen nicht wie ein Aschenbecher zu riechen.

Den Eintritt von 6 EUR ist der Spaß auf jeden Fall wert!

Deshalb: jederzeit gerne wieder!

Donnerstag Okt 01, 2009

equals() und hashCode() richtig überschreiben

In Java erben alle Objekte von der Oberklasse Object. Object implementiert eine Reihe von Methoden, die dann entsprechend in allen Java-Objekten wiederzufinden sind.

Zu diesen Methoden gehören auch equals und hashCode. Die equals-Methode wird beispielsweise in HashSet oder HashMap angewendet um sicher zu gehen, dass keine Dubletten in der Collection enthalten sind. Verwenden wir nun beispielsweise die Collection HashSet mit unseren eigenen Objekten, ohne equals zu überschreiben, so kann es passieren, dass wir von einem Produkt mehrere Instanzen mit den selben Werten in dem HashSet zu finden sind.

Der Grund dafür ist, dass HashSet die equals-Methode verwendet die einzelnen Objekte zu vergleichen. Die Implementierung von equals im „Mutter“-Objekt Object kennt natürlich nicht die Felder unseres Objektes und deren Relevanz. Aus diesem Grunde gibt die equals-Methode bei jedem vergleich zweier unterschiedlicher Instanzen unseres Objektes ein „false“, obwohl die Felder unter Umständen die gleichen Inhalten haben.

Um nun unser Objekt korrekt mit anderen Klassen wie HashSet und HashMap, die auf diese Methoden zugreifen, verwenden zu können ist es wichtig die Methoden in unseren eigenen Objekten zu überschreiben.

Wichtig: Wenn wir equals überschreiben, müssen wir auch immer hashCode überschreiben.

Angenommen wir haben die fiktive Klasse Produkt mit folgenden Feldern:

public class Produkt {
private String artikelBezeichnung;
private int artikelNr;
private BigDecimal preis;
}

Der Einfachheit halber, setze ich vorraus, dass bei jeder Instanz alle Felder initialisiert werden. Ansonsten müssten wir vor dem Vergleich der einzelnen Felder prüfen, ob das Feld überhaupt initialisiert ist um keine NullPointerExemption zu riskieren. Da das an dieser Stelle zu sehr vom Thema ablenkte behandle ich das hier nicht.

Dann würde eine korrekte Implementierung der equals Methode so aussehen:

@Override
public boolean equals(Object obj) {
Produkt produkt;
if (obj instanceof Produkt){
produkt = (Produkt) obj;
} else {
return false;
}

if (!(this.artikelBezeichnung.equalsIgnoreCase(produkt.artikelBezeichnung))){
return false;
}
if (this.artikelNr != produkt.artikelNr){
return false;
}
if (!this.preis.equals(produkt.preis)){
return false;
}

return true;
}

Was hier passiert: die Methode hat einen booleschen Rückgabewert, als entweder true oder false. In der ersten Zeile schaffen wir zunächst einmal eine Variable für ein Produkt-Objekt und nennen es produkt.

Im nächsten Schritt testen wir, ob das Objekt obj, welches der Methode als Parameter übergeben wurde, zufällig eine Instanz der Klasse Produkt ist. Falls ja, casten wir das Objekt auf die Klasse Produkt und referenzieren unsere zuvor erstellte Variable produkt auf das gecastete Objekt. Ist das Objekt keine Instanz von Produkt, brauchen wir den vergleich gar nicht erst weiter fort zu setzen und geben ein false zurück (else-Block)

Im weiteren Verlauf haben wir nun zwei Objekte der Klasse Produkt, zum einen das gecastete Objekt product und zum anderen this, also das Objekt, dessen equals-Methode aufgerufen wird. Wir vergleichen in den folgenden drei if-Blöcken, eines der Felder der beiden Produkte ungleich ist. Wenn einer der Tests positiv ist, also nur ein einziges Feld ungleich ist, wird ein false zurückgeben und die Methode wird beendet.

Erst wenn alle drei Felder getestet wurden und keiner der drei if-Blöcke zum Einsatz kam und kein false geworfen wurde, bleibt hier nur noch die Möglichkeit, dass die beiden Objekte in unserem Sinne gleich sind. Wir geben also true zurück.

Die equals-Methode darf nur unter folgenden Bedingungen ein true ausgeben:


  1. Wenn das Objekt mit sich selbst verglichen wird.

  2. Wenn es mit einem Objekt verglichen wird, dass bei einem umgekehrten vergleich ebenfalls true ausgeben würde.

  3. Wenn es mit einem dritten Objekt, dass mit dem verglichenen ebenfall gleich ist, auch gleich ist.

  4. Wenn die Ausgabe von hashCode beider Objekte gleich ist

Auf den ersten Blick klingt das alles sehr logisch, aber man kann diese Regeln doch schneller brechen, als man denkt. Also sollte man diese drei Regeln beim überschreiben von equals immer im Hinterkopf behalten.

Wenn wir nun equals überschrieben haben, dann müssen wir natürlich auch hashCode überschreiben. Und das sieht dann folgendermaßen aus:

@Override
public int hashCode() {
int result = 17;
result = 31 * result + artikelNr;
result = 31 * result + artikelBezeichnung.hashCode();
result = 31 * result + preis.hashCode();
return result;
}

Ein HashCode zweier gleicher Objekte ist ebenfalls immer gleich. Daher muss auch HashCode überschrieben werden. Umgekehrt ist es allerdings nicht zwingend vorgeschrieben, dass zwei unterschiedliche Objekte auch einen unterschiedlichen HashCode ausgeben. Man tut sich allerdings keinen Gefallen, wenn man immer die selbe Zahl ausgeben würde, auch wenn es den Regeln von HashCode entspräche. Soviel sei an dieser Stelle dazu gesagt. Wichtig ist auch, dass man als Multiplikatoren (hier 17 und 31) immer Primzahlen verwendet.

In der Praxis wäre der Code allerdings etwas kürzer. Denn der eine oder andere mag sich fragen, wozu ein Artikel eine Artikelnummer hat, wenn er anhand dieser nicht eindeutig identifiziert werden kann.

Um unser Beispiel ein wenig praxisnäher zu gestalten und auch um zu demonstrieren, weshalb es keine allgemeingültigen equals- und hashCode-Methoden gibt. Werden wir nur noch die Artikelnummern vergleichen, was dann so aussieht:

@Override
public boolean equals(Object obj) {
if (obj instanceof Produkt){
Produkt produkt = (Produkt) obj;
if (this.artikelNr == produkt.artikelNr) {
return true;
}
}
return false;
}

Ganz wichtig an dieser Stelle ist auch, dass wir unsere hashCode-Methode nach den selben Kriterien anpassen und nur noch die Artikelnummer berücksichtigen. Entsprechend kurz fällt dann auch unsere Methode aus, denn da der Rückgabewert von hashCode und unsere Artikelnummer jeweils int und obendrein eindeutig sein sollen, geben wir einfach die Artikelnummer als hashCode zurück und sind fertig:

@Override
public int hashCode() {
return this.artikelNr;
}

In diesem Zusammenhang möchte ich das Buch „Effective Java“ von Joshua Bloch (Quelle für dieses Tutorial), erschienen im Addison Wesley Verlag, empfehlen.

Vielen Dank auch an dieser Stelle an die Jungs von java-forum, die mit ihren unverblümten Kommentaren geholfen haben, das Tutorial besser zu machen.

Abschließend noch ein Tipp für diejenigen, die nicht so viel Zeit haben und Eclipse benutzen. Wenn man in Eclipse die Klasse öffnet, in der die beiden Methoden überschrieben werden sollen, kann man einfach mit der rechten Maustaste in den Quellcode klicken und per -> Source -> Generate hashCode() and equals() ... den Code generieren lassen. Dann wird auch NullPointerExemptions vorgebeugt. Selbstverständlich muss man den Code noch mal überprüfen.

Ich gebe diesen Hinweis absichtlich am Ende dieses Tutorials, denn wann immer man Code generieren läßt, sollte man wirklich selber in der Lage sein den Code zu schreiben um ihn auch ggfls. anzupassen. Grundsätzlich bin ich ein Freund sollcher Tools, weil es schlicht weg die Zeit des Programmierers spart, auch wenn die Anwendung dadurch vielleicht ein paar Millisekunden langsamer laufen könnte, als wenn der Code von einem Menschen geschrieben wäre.