This blog has moved to
http://blog.matthias-reining.com

Die bestehenden Artikel bleiben vorerst alle bei blogspot. Neue Artikel veröffentliche ich allerdings nur noch auf http://blog.matthias-reining.com

Donnerstag, 26. März 2009

Continuous Integration mit Hudson - Installationsanleitung (Standalone Konfiguration)

Continuous Integration mit Hudson: tolle Sache. Find ich richtig hübsch….

Um Hudson zu betreiben gibt es jede Menge verschieden Installations- und Konfigurationsmöglichkeiten: Standalone, innerhalb eines Webservers (Tomcat, JBoss, Glassfish), mit davor "geblendeten" Apache, usw.

Anbei eine kleine Anleitung wie ich Hudson eingerichtet habe.
Da ich den aktuellen Entwicklungssnapshot auf der gleichen VM betreibe wie Hudson, habe ich mich für die Standalone Variante von Hudson entschieden. Ich hoffe hierdurch Seiteneffekte zwischen dem Entwicklungsserver (hier JBoss) und Hudson zu reduzieren. Im gemeinsamen Betrieb unter JBoss erhielt ich bspw. PermGen Fehlermeldungen/ OutOfMemory Exceptions die ich so nicht sauber zuordnen konnte (zu ressourcenintensive Hudson Aktivitäten oder ein Speicherleak in der Applikation?).

Vorgaben

OS debian etch
JAVA ist installiert und im PATH

Eigenen User anlegen

i041:~# mkdir /data
i041:~# mkdir /data/hudson
i041:~# addgroup Hudson
Adding group `hudson' (GID 1002) ...
Done.
i041:~# useradd -g 1002 -d /data/hudson -s /bin/bash hudson
i041:~# chown hudson:hudson /data/hudson/

Bei der Useranlage (useradd) muss darauf geachtet werden, dass die GID (Option –g) der GID bei der Gruppenanlage entspricht.
Ggf. jetzt noch ein paar Einstellungen aus dem Lieblingsprofile kopieren, damit die Arbeit dann "schöner" von statten geht.

i041:~# cp /data/jboss/.bashrc /data/hudson/.
i041:~# chown -R hudson:hudson /data/hudson

Die folgenden Tätigkeiten sollten als Hudson User vorgenommen werden

i041:~# su hudson
hudson@i041:/root$ cd
hudson@i041:~$

Hudson Verzeichnis und Startskripte einrichten

hudson@i041:~$ mkdir bin
hudson@i041:~$ mkdir lib
hudson@i041:~$ mkdir log
hudson@i041:~$ cp /tmp/hudson.war lib/

Skript run.sh anlagen

hudson@i041:~/bin$ nano run.sh

#########################
CURRENT_USER=`whoami`
if [ $CURRENT_USER != "hudson" ]; then
echo "Run only valid as hudson user!... (not as $CURRENT_USER)"
exit -1;
fi

export HUDSON_HOME=/data/Hudson
export PATH="/usr/share/apache-ant/latest/bin:$PATH"
export FINDBUGS_HOME=$HUDSON_HOME/externals/findbugs-1.3.8

HTTP_PORT=8280
AJP13_PORT=8209
CONTROL_PORT=-1
LOGFILE=$HUDSON_HOME/log/hudson.log

nohup java -jar $HUDSON_HOME/lib/hudson.war \
--httpPort=$HTTP_PORT \
--ajp13Port=$AJP13_PORT \
--controlPort=$CONTROL_PORT \
> $LOGFILE 2>&1&

#########################

Skript shutdown.sh anlagen

hudson@i041:~/bin$ nano run.sh

#########################
#stop per kill. see http://winstone.sourceforge.net/#security

CURRENT_USER=`whoami`
if [ $CURRENT_USER != "hudson" ]; then
echo "Shutdown only valid as hudson user!... (not as $CURRENT_USER)"
exit -1;
fi

pid=`ps x | grep java | grep hudson | awk '{print $1}'`; kill $pid



#########################

hudson@i041:~/bin$ chmod u+x run.sh
hudson@i041:~/bin$ chmod u+x shutdown.sh

Der Hudson Server kann nun mit run.sh gestartet und mit shutdown.sh gestoppt werden. Damit sich der Hudson Server nicht mit dem JBoss auf der gleichen VM ins Gehege kommt sind die Webports entsprechend abgeändert (siehe Variable HTTP_PORT und AJP13_PORT).
Im Verzeichnis $HUDSON_HOME/log ist das log File enthalten. Falls der Server nicht hochfährt (bspw. ist eventuell der Webport bereits belegt) sollte dieses File auf alle Fälle geprüft werden!

[TODO: FindBugs Integration]

Mittwoch, 25. März 2009

JPA, Sequenzen (non-Id) und Oracle

Eine Sequenz per JPA abzufragen ist bekanntermaßen nicht so einfach – JPA 1.0 unterstützt nur Sequenzen bei der Genierung von IDs (@Id, @GeneratedValue). Eine Möglichkeit einen eindeutigen Wert trotzdem aus einer Sequenz zu bekommen ist natürlich per JPA Native Query (siehe bspw. http://stackoverflow.com/questions/277630/hibernate-jpa-sequence-non-id )

Nach kurzem googlen bekommt hier jede Menge Treffer die etwa wie folgt aussehen (für eine Oracle Sequenz):

Query query = em.createNativeQuery("select seq.nextval from dual ");
long nextNumber = new Long(((Number) query.getSingleResult()).longValue());

Leider funktioniert dies nicht (zumindest bei mir… L )

Es kommt hier immer die Fehlermeldung: ORA-00287: Sequence-Nummer hier nicht zulässig

Der Grund hierfür liegt daran, dass Hibernate (JPA unter JBoss), aus dem getSingleResult folgendes Statement baut:

select * from ( select profilenumber.nextval from dual ) where rownum <= 1 

Und dies ist leider kein korrektes SQL Statement, da hier eine Sequenz in der WHERE Klausel vorkommt…

Mit folgendem Java Code erhält man das gewünschte Ergebnis:

Query query = em.createNativeQuery("select seq.nextval from dual ");
long nextNumber = Long.valueOf(query.getResultList().get(0).toString());

Hier wird die überflüssige Select-Klammer um die eigentliche Sequenzabfrage in SQL weggelassen.


 

Donnerstag, 19. März 2009

Struts2 - Unchecked Exceptions loggen

Oft arbeitet man bei technischen Problemen oder Situation, die im Programmflow nicht vorkommen sollten, mit RuntimeExceptions (unchecked Exceptions). Ein paar Allgemeine Informationen rundum das Thema sind in folgenden Artikel schön dargestellt: http://www.roseindia.net/java/exceptions/exception.shtml.

Unchecked Exceptions müssen somit auch in der Struts Action nicht abgefangen werden. Das Struts Framework verarbeitet in diesem Fall die Exception.

Um in Struts ganz allgemein Exceptions abzufangen kann man in der struts.xml ein global exception mapping definieren. Dabei kann dann noch ein globaler result Typ eingestellt werden:

<global-results>
<result name="error">/WEB-INF/web/jsp/error.jsp</result>
</global-results>

<global-exception-mappings>
<exception-mapping exception="java.lang.Exception" result="error"/>
</global-exception-mappings>

Die error.jsp kann dann die Fehlermeldung bspw. wie folgt ausgeben:

...
<h1>ERROR</h1>
<s:actionerror/>
<p>
<s:property value="%{exception.message}"/>
</p>
<hr/>
<h3>Technical Details</h3>
<p>
<font style="font-size: 9px; color:gray">
<s:property value="%{exceptionStack}"/>
</font>
</p>
...

Die Exception wird dann allerdings nur im Client (Browser) dargestellt. Wichtig ist es aber natürlich auch, die Fehlermeldung in Logfiles zu dokumentieren um anschließend eine Ursachenforschung zu betreiben.

Um dies zentral vornehmen zu können bietet sich die globale error.jsp an. Mit folgenden Befehlen wird der Stracktrace mit Hilfe von log4j geloggt.

...
<%@page import="org.apache.log4j.Logger"%>
<s:set name="stackTrace" value="%{exceptionStack}" scope="page"/>
<%
String stackTrace = (String) pageContext.getAttribute("stackTrace");
Logger.getLogger(this.getClass()).error(stackTrace);
%>
...

In Variable exceptionStack auf dem ValueStack hat das Struts Framework den Stacktrace bereits reingeschrieben. Der restliche Code dürfte soweit selbsterklärend sein.

Samstag, 7. März 2009

EJBs und Ressourcen beim Aufruf einer „fremden“ Webapplikation per local Call

Der Titel ist vermutlich etwas unglücklich formuliert, weshalb ich hier nochmal die Ausgangslage etwas detailierter erläutere:

Ausganslage

Auf dem JBoss (Version 4.2.3) befindet sich eine EAR Applikation (MyApp.ear). In ihr enthalten sind einige Libs, ein EJB jar File (MyAppEJB.jar) und eine WAR Webapplikation (MyAppWeb.war). Die Webapplikation kennt das EJB jar (im Manifest bekannt gemacht) und kommuniziert mit den EJBs per local Aufrufen.

Um die Anwendung konfigurierbar zu halten arbeitet eine Methode innerhalb eines Stateless SessionBeans (SLSB) mit einer weiteren "normalen" Konfigurationsklasse, die ebenfalls in MyAppEJB.jar liegt. Die Hilfsklasse "Config" hat eine static Methode mit der ein Konfigurationsfile eingelesen wird. Dies passiert mit Hilfe der Apache API Commons Configuration (Configuration configuration = new PropertiesConfiguration( filePath ) ). Das Konfigurationsfile liegt innerhalb von MyAppEJB.jar im Verzeichnis META-INF (filePath = "META-INF/config.properties)

(Laut EJB Spec sollen keine Dateien vom Filesystem geladen werden bzw. es sollen keine Klassen aus java.io verwendet werden. Das File könnte in einer geclusterten Umgebung auf den unterschiedlichen Nodes unterschiedlich aussehen. Nachdem hier das File allerdings immer fix/ nicht änderbar im EAR bzw. EJB Archiv liegt, geht dies eigentlich in Ordnung. Später dazu mehr).

Soweit funktioniert hier auch alles!

Nun existiert eine weitere Webapplikation außerhalb des EARs (Bei der WebApp handelt es sich um eine Maintenance Anwendung, die auch getrennt deployed werden soll). Die Maintenance WebApp kommuniziert allerdings auch mit EJBs aus der oben beschriebenen Anwendung. Da die beiden Applikationen sich auf demselben Server befinden, findet die Kommunikation per local Interface statt (JNDI).

Problem

Falls nach dem Serverstart die Maintenance Webapp zuerst die EJB Methode aufruft, die wiederrum die Hilfsklasse "Config" aufruft und somit auch initialisiert kommt es zu folgender Fehlermeldung:

...
Caused by:
org.apache.commons.configuration.ConfigurationException: Cannot locate configuration source META-INF/config.properties
...

Die Reihenfolge ist in diesem Fall relevant, da die Konfiguration nur beim ersten Zugriff auf die Klasse initialisiert wird. Bis man das "Reihenfolgenproblem" lokalisiert hat, schaut es dummerweise so aus, als ob das Ganze manchmal geht, manchmal nicht… blöde Sache!

Hintergrund

PropertiesConfiguration versucht, falls es nirgends anders fündig wird, das File aus dem ClassPath zu lesen. In diesem Fall aus dem MyAppEJB JAR Archiv unter META-INF, was ja soweit auch korrekt ist.

Allerdings wie greift man auf ein File innerhalb eines JAR Files zu?

Thread.currentThread().getContextClassLoader.getResource(filePath)

Und genau so macht es die Apache Commons Configurations API.

Nachdem es sich um einen local Aufruf handelt, bleibt der ganze Aufruf-Stack logischerweise auch im gleichen Thread und hat somit auch den WAR Class Loader als ContextClassLoader.


 

Local Zugriff durch einer WebApplikation innerhalb des Enterprise Archives (EAR)

Bei einem Aufruf über MyAppWeb, das innerhalb von MyApp.ear liegt, funktioniert das Ganze; Die VM durchsucht den Classpath nach filePath (=META-INF/config.properties). Hier wird immer zuerst im Parent ClassLoader gesucht (dies passiert rekursiv).
Im folgendes diesen Prozess etwas ausführlicher:

Die Anfrage startet nach wie vor im Web Thread von MyAppWeb (der Threadname lautet bspw. etwa: http-127.0.0.1-8080-5; dabei wird der WAR Class Loader verwendet, Name = "WebappClassloader"). Bei der Such wird jetzt erst im Parent Classloader rekursiv gesucht:
WAR Class Loader à EJB Class Loader à Application Server Class Loader à System Class Loader à Extension Class Loader à Bootstrap Class Loader

Die Suche beginnt also im Bootstrap Class Loader – hier wird allerdings das File "META-INF/config.properties" nicht gefunden. Das Spiel geht dann so weiter bis zum EJB Class Loader. Hier wird die Classloader Resourcensuche fündig und liefert das gewünschte File als URL zurück. Die Konfiguration kann geladen werden!


 

Local Zugriff durch eine "selbstständigen" WebApplikation außerhalb des Enterprise Archives

Hier gilt das gleiche oben; bei einem Local Call befindet man sich im selben Thread. Der Classpath wird wieder rekursiv durchsucht. Dieser schaut hier wie folgt aus:
WAR Class Loader à Application Server Class Loader à System Class Loader à Extension Class Loader à Bootstrap Class Loader

In keinen der durchsuchten Classloader Resourcen wird das File "META-INF/config.properties" gefunden. Das File liegt ja auch im MyAppEJB.jar, dass hier überhaupt nicht durchsucht wird. Die Konfiguration kann nicht geladen werden!

Die Klassen "Config" wird vom AppServer bzw. vom EJB mit einen eigenen ClassLoader initialisiert (org.jboss.mx.loading.UnifiedClassLoader3@1be513c). Allerdings ist der ClassLoader vom aktuellen Thread (Thread.currentThread().getContextClassLoader) immer noch der WAR Class Loader (WebappClassLoader).


 

Problembehebung

Das Problem lässt sich auf verschieden Arten umgehen:

  • Die Apache Commons Configuration bietet auch eine Möglichkeit die Konfiguration aus der DB auszulesen (DatabaseConfiguration), so dass überhaupt keine Konfiguration in Form von Property-Files mehr notwendig ist.
  • Das Problem kann natürlich auch umgangen werden, in dem man dafür sorgt, dass die Konfiguration nach dem Start des Servers immer zuerst von der EAR Applikation initialisiert wird (funktioniert, ist aber nach Spec nicht wirklich sauber…)
  • Die Maintenance Applikation greift per remote Zugriff auf die EJBs zu. So ist sichergestellt, dass nicht der Maintenance Thread/ Classloader versucht das Konfigurationsfile zu laden.
  • Beim initialisieren von PropertiesConfiguration gibt man nicht den relativen Pfad (META-INF/config.properties) sondern gleich die URL innerhalb des EJB Archives (new PropertiesConfiguration(Config.class.getResource("/META-INF/config.properties")); )

In diesem Projekt habe ich den pragmatischen Ansatz gewählt und mich für die letzte Variante entschieden