ETag

In einer unserer Webapplikationen kann der Benutzer ein Image (GIF,PNG) uploaden welches auf jeder HTML Seite der Applikation angezeigt wird. Dieses Bild wird in der Datenbank abgelegt und wenn benötigt via Servlet zum Client gesendet.

Eine erste Implementation dieses Servlets sah folgendermassen aus:

public class LogoServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
                       throws ServletException, IOException {

    byte[] imageDate = .... //Bild holen. Datenbankzugriff

    if (imageDate != null) {
      response.setContentType(...);
      OutputStream out = response.getOutputStream();
      out.write(imageDate);
      out.close();
    }
  }
}

Die web.xml Datei enthält folgenden Eintrag.

  <servlet>
    <servlet-name>logo</servlet-name>
    <servlet-class>LogoServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>logo</servlet-name>
    <url-pattern>/logo.gif</url-pattern>
  </servlet-mapping>

Da dieses Bild auf jeder Seite eingebunden ist, wird nun jedesmal wenn der Benutzer eine neue Seite aufruft auch jedesmal das ganze Image zum Client gesendet. Da sich dieses Bild praktisch nie ändert könnte der Browser das Bild cachen und der Server müsste es nicht jedesmal senden.

 

Zu diesem Zweck wurde in HTTP/1.1 der ETag (Entity Tag) eingeführt. Dazu sendet der Server für statische und dynamische Inhalte (welche sich selten ändern) im Header der Response einen ETag mit. Der ETag kann auf verschiedene Arten berechnet werden. Eine Methode ist es das Änderungsdatum zu verwenden oder man berechnet zum Beispiel mit MD5 einen Hash von der Datei.

 

Der Browser kann nun mit Hilfe dieses ETag die erhaltene Information im Cache ablegen. Wenn er die Information erneut benötigt sendet der Browser den ETag im Request (If-None-Match) mit und der Server entscheidet dann aufgrund des ETag ob sich die Daten in der Zwischenzeit verändert haben. Wenn sich nichts geändert hat sendet der Server nur eine Response mit dem Code 304 Not Modified und einem leeren Body zurück.

 

Statische Inhalte (Images, usw.) die sich in einer Webapplikation befinden werden vom Container (z.B. Tomcat) nach diesem Prinzip ausgeliefert. Dynamische Inhalte wie unser Image das der Benutzer uploaden kann müssen dagegen selber behandelt werden. Folgendes Servlet zeigt wie ein ETag Handling aussehen könnte.

public class LogoServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String etag = ... //ETag holen. Kann zum Beispiel im Speicher abgelegt sein.
    if (etag != null) {
      String previousToken = request.getHeader("If-None-Match");
      String token = '"' + etag + '"';

      //ETag bei jeder Antwort mitsenden
      response.setHeader("ETag", token);

      if (previousToken != null && previousToken.equals(token)) {
        response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
      } else {
        response.setContentType(...);
        byte[] imageDate = .... //Bild holen. Datenbankzugriff
        OutputStream out = response.getOutputStream();
        out.write(imageDate);
        out.close();
      }
    }
  }
}

Weitere Links zum Thema

http://en.wikipedia.org/wiki/HTTP_ETag
http://caucho.com/resin/doc/proxy-cache.xtp
http://www.infoq.com/articles/etags
http://www.oreillynet.com/onjava/blog/2004/07/optimizing_http_downloads_in_j.html

Office 2007 Mime Types

Wir mussten in einer Webapplikation einen Excel Report erstellen. Der Benutzer kann vorab wählen in welchem Format der Report erstellt werden soll. Er kann zwischen Excel 2003 (xls) und Excel 2007 (xlsx) wählen.

Den MIME Type haben wir zuerst so gesetzt.

response.setContentType("application/vnd.ms-excel");

Mit folgendem Statement wird erzwungen das ein File Download Dialog erscheint. Dateien mit bekanntem MIME Type werden ansonsten direkt im Browserfenster geöffnet (http://support.microsoft.com/kb/260519)

response.addHeader("Content-disposition", "attachment;filename=test.xls");

Dies funktioniert für das Excel 2003 Format ohne Probleme. Wenn wir nun eine Excel 2007 Format exportieren sendet das Programm diesen Header mit xlsx als Endung.

response.addHeader("Content-disposition", "attachment;filename=test.xlsx");

Mit diesem Setup erschien in Excel eine Warnung “The file you are to open ‘test.xlsx-1.xls’, is in a different format thant specified by the file extension”.

Das Problem ist das für Excel 2007 ein anderer MIME Type angegeben werden muss.

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

Mit diesem MIME Type wird die generierte Datei ohne Probleme in Excel geöffnet.

Hier eine Auflistung von allen MIME Typen für die Office 2007 Formate.

.docm application/vnd.ms-word.document.macroEnabled.12
.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
.dotm application/vnd.ms-word.template.macroEnabled.12
.dotx application/vnd.openxmlformats-officedocument.wordprocessingml.template
.potm application/vnd.ms-powerpoint.template.macroEnabled.12
.potx application/vnd.openxmlformats-officedocument.presentationml.template
.ppam application/vnd.ms-powerpoint.addin.macroEnabled.12
.ppsm application/vnd.ms-powerpoint.slideshow.macroEnabled.12
.ppsx application/vnd.openxmlformats-officedocument.presentationml.slideshow
.pptm application/vnd.ms-powerpoint.presentation.macroEnabled.12
.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation
.xlam application/vnd.ms-excel.addin.macroEnabled.12
.xlsb application/vnd.ms-excel.sheet.binary.macroEnabled.12
.xlsm application/vnd.ms-excel.sheet.macroEnabled.12
.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.xltm application/vnd.ms-excel.template.macroEnabled.12
.xltx application/vnd.openxmlformats-officedocument.spreadsheetml.template

Tomcat 6.0.24

Gestern ist der neue Tomcat 6.0.24 erschienen. Dies ist die erste Version welche die 64-Bit Versionen des Windows Services mitliefert.

Der Workaround wie ich ihn in meinem Blog vom 6.5.2009 beschrieben habe ist daher nicht mehr nötig.

Die Windows Installer Version, welche auf der Downloadseite heruntergeladen werden kann, installiert je nach System die richtigen Exe-Files.

Was sich seit der letzten veröffentlichten Version 6.0.20 auch noch geändert hat liest man im umfangreichen Changelog.

Spring 3.0.0

Diese Woche (16.12.2009) ist Spring 3.0.0 erschienen. Das Framework lässt sich wie bisher von der Projektseite herunterladen. Änderungen zum letzten Release Candidat findet man im Changelog. Zum Beispiel werden Komponenten, die mit javax.annotation.ManagedBean annotiert sind, nun auch vom Scanner erkannt und in den Context eingebunden.

Die grossen Neuerungen in der Version 3.0.0 hat Jürgen Hoeller in seinem Blog beschrieben.

Im Blog von Eyal Lupu findet man eine Präsentation über die Neuerungen von Spring 3.0.0. Im weiteren schreibt er hier und hier über die neue Expression Language.

Die Spring 3 Artefakte sind im zentralen Maven Repository und im Enterprise Bundle Repository, das von SpringSource betrieben wird, abgelegt. In diesem Blogeintrag wird beschrieben wie man Maven konfiguriert um mit dem neuen Spring Release zu arbeiten.

Eine weitere interessante Seite die sich mit Spring beschäftig ist Spring by Example. Wie der Name sagt findet man hier viele Beispiele wie Spring für bestimmte Anwendungsfälle konfiguriert werden muss.

Versionierung mit Hibernate Envers

Das Ziel von Envers (Entity Versioning) ist es eine Historisierung von Entity-Klassen anzubieten. Um die Versionierung einzuschalten müssen nur die Klassen oder deren Felder mit @Audited annotiert werden.

Envers kennt, ähnlich wie Subversion, eine globale Revisionsnummer. Jede Transaktion, die eine Änderung auf der Datenbank vornimmt, entspricht einer Revision und inkrementiert die Revisionsnummer. Wenn man die Revisionsnummer kennt kann man damit die Entitäten für diese Revision abfragen. Man kann zu jeder Revisionsnummer herausfinden zu welchem Zeitpunkt diese erstellt wurde und umgekehrt kann für einen Zeitpunkt die entsprechende Revisionsnummer herausgefunden werden.

Envers funktioniert zusammen mit Hibernate und JPA. Ab Hibernate 3.5 ist die Envers Library in der Hibernate Core Library integriert.

Um Envers einzuschalten müssen als ersten Schritt Event Listeners konfiguriert werden. Diese werden in die die Datei persistence.xml (JPA) oder hibernate.cfg.xml (Hibernate) eingetragen.

Für dieses Beispiel wird JPA verwendet, deshalb werden die Einträge für den AuditEventListener in die Datei persistence.xml eingetragen.

<?xml version="1.0" encoding="UTF-8"?>
<persistence ... >

  <persistence-unit name="defaultDatabase" ...>
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>

      <property name="hibernate.hbm2ddl.auto" value="update" />

      <property name="hibernate.ejb.event.post-insert"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.post-update"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.post-delete"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.pre-collection-update"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.pre-collection-remove"
                value="org.hibernate.envers.event.AuditEventListener" />
      <property name="hibernate.ejb.event.post-collection-recreate"
                value="org.hibernate.envers.event.AuditEventListener" />
      ...

    </properties>
  </persistence-unit>
</persistence>

Wird nun eine Klasse mit @Audited annotiert werden alle Felder versioniert. Alternativ können auch nur bestimme Properties annotiert werden, dann werden auch nur diese historisiert.

In diesem Beispiel werden zwei Entity Klassen Firma und Mitarbeiter verwendet. Jede Firma kennt mehrere Mitarbeiter. In der Entität Firma wird nur das Feld strasse und in der Mitarbeiter Klasse werden alle Properties versioniert.

@Entity
public class Firma {
  @Id
  @GeneratedValue
  private int id;

  private String name;

  @Audited
  private String strasse;

  private String ort;

  @OneToMany(mappedBy = "firma", cascade=CascadeType.ALL, orphanRemoval=true)
  private Set<Mitarbeiter> mitarbeiter;

  //getter und setter Methoden

}
@Entity
@Audited
public class Mitarbeiter {
  @Id
  @GeneratedValue
  private int id;

  private String name;
  private String vorname;
  private String strasse;
  private String ort;

  @ManyToOne
  private Firma firma;

  //getter und setter Methoden
}

Es kann nun ganz normal mit den Entitäten gearbeitet werden. Erstellen, Modifizieren und Löschen funktioniert wie bisher. Das Schema das für diese beiden Entitäten erstellt wird ist das gleiche was auch ohne Versionierung erstellt würde. Zusätzlich erstellt Envers automatisch drei Tabellen: Firma_AUD, Mitarbeiter_AUD und REVINFO. In diese Tabellen werden die Versionsinformationen abgespeichert.

Mit dem folgenden Programm wird eine Firma und zwei Mitarbeiter, die für diese Firma arbeiten, in die Datenbank eingefügt. Die Transaktion erhält die Revisionsnummer 1.

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("defaultDatabase");
    EntityManager em = emf.createEntityManager();

    //Revision 1
    em.getTransaction().begin();

    Firma firma = new Firma();
    firma.setName("Company A");
    firma.setOrt("Zurich");
    firma.setStrasse("Street");

    Set<Mitarbeiter> mitarbeiterSet = new HashSet<Mitarbeiter>();

    Mitarbeiter mitarbeiter = new Mitarbeiter();
    mitarbeiter.setFirma(firma);
    mitarbeiter.setName("Muster");
    mitarbeiter.setVorname("Felix");
    mitarbeiter.setOrt("Zurich");
    mitarbeiter.setStrasse("Seestrasse 1");
    mitarbeiterSet.add(mitarbeiter);

    mitarbeiter = new Mitarbeiter();
    mitarbeiter.setFirma(firma);
    mitarbeiter.setName("Meier");
    mitarbeiter.setVorname("Jolanda");
    mitarbeiter.setOrt("Bern");
    mitarbeiter.setStrasse("Bahnhofstrasse 10");
    mitarbeiterSet.add(mitarbeiter);

    firma.setMitarbeiter(mitarbeiterSet);

    em.persist(firma);    

    em.getTransaction().commit();

    em.close();
    emf.close();

Die erstellten Tabellen und deren Inhalt sehen wie folgt aus.

Firma
id name ort strasse
1 Company A Zurich Street


Firma_AUD
id REV REVTYPE strasse
1 1 0 Street


Mitarbeiter
id name ort strasse vorname firma_id
1 Muster Zurich Seestrasse 1 Felix 1
2 Meier Bern Bahnhofstrasse 10 Jolanda 1


Mitarbeiter_AUD
id REV REVTYPE name ort strasse vorname firma_id
1 1 0 Muster Zurich Seestrasse 1 Felix 1
2 1 0 Meier Bern Bahnhofstrasse 10 Jolanda 1


REVINFO
REV REVTSTMP
1 1260247116265



Die *_AUD Tabellen enthalten die Versionen der Entitäten. Zusätzlich wird die Revisionsnummer (REV) und der Typ der Revision (0=ADD, 1=MODIFY, 2=DELETE) abgespeichert. Die Tabelle REVINFO speichert zu jeder Revision den Zeitpunkt ab wann diese erstellt wurde.

Updates

Mit dem folgenden Program werden verschiedene Updates und Delete Operationen ausgeführt.

  • Das Feld Strasse der Firma wird von “Street” auf “Neue Strasse” geändert
  • Es wird ein neuer Mitarbeiter mit Namen Andreas Müller (id=3) eingefügt
  • Im Mitarbeiter Felix Muster (id=1) wird die Strasse auf “Seestrasse 1″ und der Ort auf “Biel” geändert.
  • Die Mitarbeiterin Jolanda Meier (id=2) wird gelöscht

Jedes dieser Updates wird in einer eigenen Transaktion ausgeführt. Es werden deshalb 4 Revisionen erstellt.

 public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("defaultDatabase");
    EntityManager em = emf.createEntityManager();

    //Update Firma Revision 2
    em.getTransaction().begin();
    Firma firma = updateFirma(em);
    em.getTransaction().commit();

    //Neuer Mitarbeiter Revision 3
    em.getTransaction().begin();
    addMitarbeiter(firma);
    em.getTransaction().commit();

    //Mitarbeiter updaten Revision 4
    em.getTransaction().begin();
    updateMitarbeiter(em);
    em.getTransaction().commit();

    //Mitarbeiter löschen Revision 5
    em.getTransaction().begin();
    deleteMitarbeiter(em);
    em.getTransaction().commit();

    em.close();
    emf.close();

  }

  private static Firma updateFirma(EntityManager em) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Firma> c = cb.createQuery(Firma.class);
    Root<Firma> p = c.from(Firma.class);

    Predicate condition = cb.equal(p.get(Firma_.name), "Company A");
    c.where(condition);

    TypedQuery<Firma> q = em.createQuery(c);
    Firma firma = q.getSingleResult();

    firma.setStrasse("Neue Strasse");
    return firma;
  }

  private static void addMitarbeiter(Firma firma) {
    Mitarbeiter mitarbeiter = new Mitarbeiter();
    mitarbeiter.setFirma(firma);
    mitarbeiter.setName("Müller");
    mitarbeiter.setVorname("Andreas");
    mitarbeiter.setOrt("Olten");
    mitarbeiter.setStrasse("Rheinstrasse 10");
    firma.getMitarbeiter().add(mitarbeiter);
  }

  private static void updateMitarbeiter(EntityManager em) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Mitarbeiter> cq = cb.createQuery(Mitarbeiter.class);
    Root<Mitarbeiter> root = cq.from(Mitarbeiter.class);
    Predicate condition = cb.and(cb.equal(root.get(Mitarbeiter_.name), "Muster"),
                          cb.equal(root.get(Mitarbeiter_.vorname), "Felix"));
    cq.where(condition);

    TypedQuery<Mitarbeiter> q = em.createQuery(cq);
    Mitarbeiter mitarbeiter = q.getSingleResult();
    mitarbeiter.setStrasse("Hauptstrasse 10");
    mitarbeiter.setOrt("Biel");
  }

  private static void deleteMitarbeiter(EntityManager em) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Mitarbeiter> cq = cb.createQuery(Mitarbeiter.class);
    Root<Mitarbeiter> root = cq.from(Mitarbeiter.class);
    Predicate condition = cb.and(cb.equal(root.get(Mitarbeiter_.name), "Meier"),
                          cb.equal(root.get(Mitarbeiter_.vorname), "Jolanda"));
    cq.where(condition);

    TypedQuery<Mitarbeiter> q = em.createQuery(cq);
    Mitarbeiter mitarbeiter = q.getSingleResult();
    mitarbeiter.getFirma().getMitarbeiter().remove(mitarbeiter);
    em.remove(mitarbeiter);
  }

Nach diesen Operationen sehen die Inhalte der Tabellen wie folgt aus

Firma
id name ort strasse
1 Company A Zurich Neue Strasse


Mitarbeiter
id name ort strasse vorname firma_id
1 Muster Zurich Seestrasse 1 Felix 1
2 Meier Bern Bahnhofstrasse 10 Jolanda 1

Keine Überraschung in den beiden Tabellen Firma und Mitarbeiter. Diese enthalten den aktuellen Stand der Daten.

Firma_AUD
id REV REVTYPE strasse
1 1 0 Street
1 2 1 Neue Strasse

Die Firma_AUD Tabelle enthält einen zusätzlichen Eintrag für die Änderung der Strasse. REVTYPE=1 zeigt an das dies ein Update war. Die Änderung wurde in der Revision 2 durchgeführt.

Mitarbeiter_AUD
id REV REVTYPE name ort strasse vorname firma_id
1 1 0 Muster Zurich Seestrasse 1 Felix 1
2 1 0 Meier Bern Bahnhofstrasse 10 Jolanda 1
3 3 0 Müller Olten Rheinstrasse 10 Andreas 1
1 4 1 Muster Biel Hauptstrasse 10 Felix 1
2 5 2 NULL NULL NULL NULL NULL

In dieser Tabelle befinden sich drei zusätzliche Einträge. Der Eintrag mit der Revision 3 ist das Einfügen des neuen Mitarbeiters (REVTYPE=0). Die Revision 4 ist die Änderung der Strasse und des Ortes (REVTYPE=1). In der Revision 5 wurde der Mitarbeiter gelöscht (REVTYPE=2). Beim Löschen einer Entität sind alle Felder in der *_AUD Tabelle mit NULL abgefüllt.

REVINFO
REV REVTSTMP
1 1260247116265
2 1260248305640
3 1260248305671
4 1260248305687
5 1260248305703

Da jede Änderung in einer eigenen Transaktion abgewickelt wurde, sind 4 zusätzliche Revisionen entstanden.

Abfragen

Es gibt grundsätzlich zwei Abfragen die man über die historisierten Daten machen kann. Man kann sich den Stand einer Entität zu einer angegebenen Revisionsnummer geben lassen. Oder man erhält eine Liste von den Revisionen in denen eine bestimme Entität geändert wurde.

Im ersten Beispiel wird die Firma abgefragt, wie sie zum Zeitpunkt der Revision 1 ausgesehen hat.

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("defaultDatabase");
    EntityManager em = emf.createEntityManager();

    AuditReader reader = AuditReaderFactory.get(em);
    Firma oldFirma = reader.find(Firma.class, 1, 1);

    System.out.println(oldFirma.getStrasse()); //Output: street
    System.out.println(oldFirma.getName()); //Output: null

Der find Methode müssen drei Parameter übergeben werden. Die Klasse der Entität, der Primary Key und die Revisionsnummer. Das Feld strasse enhält nun die Information wie sie zum Zeitpunkt der Revision 1 (Insert) ausgesehen hat. Alle anderen Felder im Objekt sind null da wir Firma so konfiguriert haben das nur das Feld strasse versioniert wird.

Im folgenden Beispiel werden alle Revisionen aufgelistet, welche eine Änderung der Firma mit Primary Key 1, enthalten.

  List<Number> revisions = reader.getRevisions(Firma.class, 1);
  for (Number rev : revisions) {
    System.out.println(rev);
  }
  //Output: 1 2

Das Datum zu einer bestimmten Revision erhält man mit folgender Methode. Als Parameter übergibt man die Revisionsnummer

reader.getRevisionDate(1);

Mit der getRevisionNumberForDate(Date) erhält man eine Revision zu einem bestimmten Datum. Diese Methode wirft eine org.hibernate.envers.exception.RevisionDoesNotExistException wenn zum angegebenen Datum keine Revision gefunden wurde.

    Calendar cal = new GregorianCalendar(2009, Calendar.DECEMBER, 6);
    System.out.println(reader.getRevisionNumberForDate(cal.getTime()));

Dieses Beispiel listet alle Mitarbeiter auf welche zu der Firma 1 gehören und zum Zeitpunkt der Revision 1 vorhanden waren:

    AuditQuery query = reader.createQuery().forEntitiesAtRevision(Mitarbeiter.class, 1);
    query.add(AuditEntity.relatedId("firma").eq(1));

    List<Mitarbeiter> mitarbeiterList = query.getResultList();
    for (Mitarbeiter mitarbeiter : mitarbeiterList) {
      System.out.println(mitarbeiter.getName());
    }
    //Output: Muster Meier

Wie man sieht wird hier auch der Mitarbeiter Meier angezeigt. Dieser wurde erst in der Revision 5 gelöscht.

Das folgende Beispiel listet mit Hilfe der Methode forRevisionsOfEntity alle Revisionen von allen Mitarbeitern auf. Das Query liefert eine Liste von Objektarrays mit drei Elementen zurück. Das erste Element beinhaltet die geänderte Entität. Das zweite Element ist eine Entität welche die Revsionsdaten (Nummer und Zeitpunkt) beinhaltet. Wenn nichts speziell konfiguriert wurde ist diese vom Typ DefaultRevisionEntity. Das dritte Element ist der Typ der Revision. Einer von drei Werten der RevisionType Enumeration (ADD, MOD, DEL).

    query = reader.createQuery().forRevisionsOfEntity(Mitarbeiter.class, false, true);
    List<Object[]> rersults = query.getResultList();
    for (Object[] result : rersults) {
      Mitarbeiter mitarbeiter = (Mitarbeiter)result[0];
      DefaultRevisionEntity revEntity = (DefaultRevisionEntity)result[1];
      RevisionType revType = (RevisionType)result[2];

      System.out.println("Revision     : " + revEntity.getId());
      System.out.println("Revision Date: " + revEntity.getRevisionDate());
      System.out.println("Type         : " + revType);
      System.out.println("Mitarbeiter  : " + mitarbeiter.getName());

      System.out.println("------------------------------------------------");
    }

Der Output für dieses Beispiel wird folgendermassen aussehen. Das Revision Date ist natürlich abhängig vom Zeitpunkt wann das Beispiel ausgeführt wird.

Revision     : 1
Revision Date: Tue Dec 08 05:38:36 CET 2009
Type         : ADD
Mitarbeiter  : Muster
------------------------------------------------
Revision     : 1
Revision Date: Tue Dec 08 05:38:36 CET 2009
Type         : ADD
Mitarbeiter  : Meier
------------------------------------------------
Revision     : 3
Revision Date: Tue Dec 08 05:58:25 CET 2009
Type         : ADD
Mitarbeiter  : Müller
------------------------------------------------
Revision     : 4
Revision Date: Tue Dec 08 05:58:25 CET 2009
Type         : MOD
Mitarbeiter  : Muster
------------------------------------------------
Revision     : 5
Revision Date: Tue Dec 08 05:58:25 CET 2009
Type         : DEL
Mitarbeiter  : null
------------------------------------------------

Weitere Informationen zu Envers findet man auf der Projektseite.

Datenbankmigration mit Liquibase und DbUnit

Wir mussten mehrere Microsoft SQL Server Datenbanken nach MySQL auf einem Linux Server zügeln. Zu diesem Zweck setzten wir die Java Libraries Liquibase und DbUnit ein.

Folgende Libraries werden benötigt.

Liquibase 2.0 RC1
DbUnit 2.4.7
JTDS 1.2.4 JDBC Treiber für MSSQL Server
MySQL Connector 5.1.10 JDBC Treiber für MySQL
SL4J 1.5.8
Logback 0.9.17

Es werden keine zusätzlichen, selber erstellten Programme benötigt. Alle Aufgaben können mit Ant erledigt werden.

Zu Beginn werden die Angaben für die Datenbankverbindungen in der Datei build.properties definiert.

db1.driver=net.sourceforge.jtds.jdbc.Driver
db1.url=jdbc:jtds:sqlserver://localhost/sourcedb
db1.user=user
db1.password=password

db2.driver=com.mysql.jdbc.Driver
db2.url=jdbc:mysql://linuxserver/targetdb
db2.user=user
db2.password=password

In der Ant Build Datei build.xml wird build.properties eingelesen und die Tasks für DbUnit und Liquibase definiert.

<project name="migration">
  <property file="${basedir}/build.properties" />
  <property name="lib.dir" value="${basedir}/lib" />

  <path id="tool.classpath">
    <path location="${lib.dir}/dbunit-2.4.7.jar" />
    <path location="${lib.dir}/jtds-1.2.4.jar" />
    <path location="${lib.dir}/liquibase.jar" />
    <path location="${lib.dir}/mysql-connector-java-5.1.10-bin.jar" />
    <path location="${lib.dir}/jcl-over-slf4j-1.5.8.jar" />
    <path location="${lib.dir}/slf4j-api-1.5.8.jar" />
    <path location="${lib.dir}/logback-classic-0.9.17.jar" />
    <path location="${lib.dir}/logback-core-0.9.17.jar" />
    <pathelement location="${basedir}"/>
  </path>  

  <taskdef resource="liquibasetasks.properties" classpathref="tool.classpath"/>
  <taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask" classpathref="tool.classpath"/>  

...

</project>

Als nächster Schritt wird das Schema der Quelldatenbank exportiert. Dazu wird der Liquibase Task <generateChangeLog> eingesetzt.

<project name="migration">
...
  <target name="generate.changelog">
    <generateChangeLog
            outputFile="${basedir}/ddl.xml"
            driver="${db1.driver}"
            url="${db1.url}"
            username="${db1.user}"
            password="${db1.password}"
            classpathref="tool.classpath" />
    <replace file="${basedir}/ddl.xml" token="int identity" value="int"/>
    <replace file="${basedir}/ddl.xml" token="baseTableSchemaName="dbo" " value=""/>
    <replace file="${basedir}/ddl.xml" token="schemaName="dbo" " value=""/>
    <replace file="${basedir}/ddl.xml" token="image" value="longtext"/>
  </target>
...
</project>

Da der Output noch SQL Server spezifische Dinge enthält muss die Datei ddl.xml angepasst werden. Der <replace> Task von Ant erledigt dies. Hier werden alle schemaName und baseTableSchemaName Attribute aus dem XML gelöscht. Zusätzlich werden alle Datentypen “int identity” durch “int” ersetzt. Bei den int identity Feldern ist das autoIncrement Attribute auf true gesetzt so dass in der MySQL Datenbank auch Autonummerfelder erzeugt werden.
Der Datentyp image für Blob Felder muss auch angepasst werden. Hier muss von Fall zu Fall enschieden werden, da es in MySQL vier verschiedene Typen (Grössen) für Blobs gibt. Das Beispiel ersetzt image durch longtext. http://dev.mysql.com/doc/refman/5.0/en/blob.html

Das erstellt Schema (ddl.xml) wird nun mit dem Liquibase Task <updateDatabase> in die Zieldatenbank eingefügt.

<project name="migration">
...
  <target name="update.db">
    <updateDatabase
            changeLogFile="${basedir}/ddl.xml"
            driver="${db2.driver}"
            url="${db2.url}"
            username="${db2.user}"
            password="${db2.password}"
            promptOnNonLocalDatabase="false"
            dropFirst="false"
            classpathref="tool.classpath"/>
  </target>
...
</project>

Falls es Fehler beim Erstellen des Schemas gibt muss die Datei ddl.xml angepasst werden.

Mit dem nächsten Schritt werden alle Daten aus der Quelldatenbank exportiert. Der <export> Task von DbUnit übernimmt diese Aufgabe.

<project name="migration">
...
  <target name="export.data">
    <dbunit driver="${db1.driver}"
        url="${db1.url}"
        userid="${db1.user}"
        password="${db1.password}">

      <dbconfig>
        <property name="datatypeFactory" value="org.dbunit.ext.mssql.MsSqlDataTypeFactory" />
      </dbconfig>      

      <export dest="${basedir}/dataexport.xml" />

    </dbunit>
  </target>
...
</project>

Die exportierten Daten werden in einer XML Datei (dataexport.xml) abgespeichert. Diese Datei kann SQL Server spezifische Daten enthalten, wie zum Beispiel dtproperties oder sysdiagrams. Diese Daten müssen aus der Datei gelöscht werden ansonsten funktioniert der anschliessende Import nicht. Mit dem <table> Parameter ist es möglich dem <export> Task anzugeben welche Tabellen exportiert werden sollen.

<export dest="...">
  <table name="TABLE1"/>
  <table name="TABLE2"/>
</export>

Weitere Informationen findet man auf der DbUnit Seite.

Im letzten Schritt werden die Daten in die Zieldatenbank importiert. Für diese Aufgabe wird der <operation> Task von DbUnit verwendet. Als Typ wird hier CLEAN_INSERT verwendet, was bedeutet das vor dem Insert alle Rows in der Tabelle gelöscht werden. Eine Auflistung aller möglichen Typen findet man in der DbUnit Dokumentation.

<project name="migration">
...
  <target name="import.data">
    <dbunit driver="${db2.driver}"
        url="${db2.url}?sessionVariables=FOREIGN_KEY_CHECKS=0"
        userid="${db2.user}"
        password="${db2.password}">

      <dbconfig>
        <property name="datatypeFactory" value="org.dbunit.ext.mysql.MySqlDataTypeFactory" />
        <property name="escapePattern" value="`?`"/>
      </dbconfig>

      <operation type="CLEAN_INSERT" transaction="true" src="${basedir}/dataexport.xml"/>

    </dbunit>
  </target>
...
</project>

Für den Import wird die Foreign Key Prüfung temporär ausgeschaltet (?sessionVariables=FOREIGN_KEY_CHECKS=0). Damit ist es möglich die Daten in einer beliebigen Reihenfolge einzufügen.

Alternativ ist es möglich die Daten bereits in der richtigen Reihenfolge zu exportieren, so dass beim Import keine Foreign Key Verletzungen auftreten. Dazu wird das Attribute ordered beim <export> Task angegeben: <export dest=”${basedir}/dataexport.xml” ordered=”true” /> Dies funktioniert allerdings nicht in allen Fällen. Wenn zum Beispiel Tabellen mit Referenzen auf sich selber (Abbildung von Hierarchien) eingesetzt werden.

Wenn für Tabellen und Felder Namen verwendet werden die in MySQL Schlüsselwörter sind dann müssen diese beim Import mit einem ` umschlossen werden. Dies wird mit der Zeile <property name="escapePattern" value="`?`"/> konfiguriert. Das Fragezeichen wird mit dem Namen der Tabelle oder Feld ersetzt.

JSR-330 mit Spring

Mit JSR-330 werden Annotationen, die für die Dependency Injection verwendet werden, standardisiert. Der Final Release ist am 14. Oktober 2009 erschienen. Den Sourcecode findet man auf der Projektseite atinject.

Spring 3.0.0.RC2 ist vollständig JSR-330 kompatibel und besteht auch die Tests des Technology Compatibility Kit (TCK) (siehe Spring Framework 3.0 RC2 released).

Weitere Libraries die JSR-330 unterstützen sind Weld (JSR-299) und zukünftige Versionen von Guice.

Für das folgende Beispiel werden diese Libraries benötigt.

<dependency org="org.springframework" name="spring-core" rev="3.0.0.RC2"/>
<dependency org="org.springframework" name="spring-beans" rev="3.0.0.RC2"/>
<dependency org="org.springframework" name="spring-context" rev="3.0.0.RC2"/>
<dependency org="org.springframework" name="spring-asm" rev="3.0.0.RC2"/>
<dependency org="org.springframework" name="spring-expression" rev="3.0.0.RC2"/>

Und natürlich das JSR-330 Jar. Dieses befindet sich im Maven Central Repository und kann mit folgenden Angaben heruntergeladen werden.

<dependency org="javax.inject" name="javax.inject" rev="1" />

Im applictaionContext.xml genügt es ein <context:component-scan …> einzufügen. JSR-330 wird automatisch aktiviert wenn sich das javax.inject Jar im Klassenpfad befindet.

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

http://www.springframework.org/schema/context

                           http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:component-scan base-package="ch.ralscha.test" />
</beans>

Als Beispiel erstellen wir ein Interface RandomGenerator, welches eine Methode randomInt definiert, und dazu zwei Implementation (NormalRandomGenerator und SecureRandomGenerator).

package ch.ralscha.test;
public interface RandomGenerator {
  int randomInt();
}

Die beiden Implementationen werden mit @Named annotiert. In Spring kann @Named wie @Service oder @Component verwendet werden. Wenn @Named kein Parameter mitgegeben wird entsprechen die Namen der Beans den Namen der Klassen. Dabei wird der erste Buchstabe klein geschrieben. Die Klasse NormalRandomGenerator wird also mit dem Namen “normalRandomGenerator” im Context abgelegt.

package ch.ralscha.test;
import java.util.Random;
import javax.inject.Named;
@Named
public class NormalRandomGenerator implements RandomGenerator {

  private Random random;

  public NormalRandomGenerator() {
    random = new Random();
  }

  public int randomInt() {
    return random.nextInt(100);
  }
}

Die Klasse SecureRandomGenerator wird mit dem Bean Namen “secure” im Context abgelegt.

package ch.ralscha.test;
import java.security.SecureRandom;
import javax.inject.Named;
@Named("secure")
public class SecureRandomGenerator implements RandomGenerator {

  private SecureRandom random;  

  public SecureRandomGenerator() {
    random = new SecureRandom();
  }

  public int randomInt() {
    return random.nextInt(100);
  }
}

Als letztes erstellen wir eine Klasse Calculation in welcher der RandomGenerator injiziert wird, dazu wird @Inject verwendet. @Inject entspricht der Spring eigenen Annotation @Autowired.

Da zwei Implementationen des RandomGenerators existieren muss angegeben werden welche davon injiziert werden soll. Dies geschieht mit der Annotation @Named und als Parameter wird der Name des Beans übergeben. Dies entspricht der Spring eigenen Annotation @Qualifier.

package ch.ralscha.test;
import javax.inject.Inject;
import javax.inject.Named;

@Named
public class Calculation {

  @Inject
  @Named("secure")
  private RandomGenerator randomGenerator;

  //@Inject
  //@Named("normalRandomGenerator")
  //private RandomGenerator randomGenerator;

  public void doSomething() {
    System.out.println(randomGenerator.randomInt());
  }
}

Das Beispiel kann nun wie folgt gestartet werden:

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculation calc = ctx.getBean("calculation", Calculation.class);
calc.doSomething();

Das Beispiel funktioniert auch ohne die Datei applicationContext.xml indem man die neue Context Klasse AnnotationConfigApplicationContext verwendet.

ApplicationContext ctx = new AnnotationConfigApplicationContext("ch.ralscha.test");
Calculation calc = ctx.getBean("calculation", Calculation.class);
calc.doSomething();

Die obengenannte Methode, Beans mit @Inject @Named(“…”) zu injizieren, hat natürlich mehrere Nachteile. Man kann sich verschreiben. @Inject @Named(“secured”) würde nicht funktionieren und eine Exception zur Laufzeit wäre die Folge. Des weiteren ist diese Lösung nicht Refactoring-freundlich. Wenn zum Beispiel NormalRandomGenerator neu AVeryNormalRandomGenerator heisst, würde der Code auch nicht mehr funktionieren. Um all diese Nachteile zu vermeiden gibt es in JSR-330 die Möglichkeit sogenannte Qualifier Annotationen zu verwenden. Dazu wird die Annotation @javax.inject.Qualifier eingesetzt. Nicht zu verwechseln mit der Spring eigenen @Qualifier Annotation.

Da zwei Implementationen des Interface RandomGenerator existieren, werden zwei Qualifier Annotationen benötigt. Dies sind normale Annotationen die mit @javax.inject.Qualifier annotiert werden.

package ch.ralscha.test;
@javax.inject.Qualifier
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Normal {}
package ch.ralscha.test;
@javax.inject.Qualifier
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Secure {}

Nun werden die Implementationen mit der entsprechenden Annotationen annotiert.

@Named @Secure
public class SecureRandomGenerator implements RandomGenerator {
...
}
@Named @Normal
public class NormalRandomGenerator implements RandomGenerator {
...
}

In der Klasse Calculation wird die Qualifier Annotation für das injizieren des Objektes verwendet.

@Named
public class Calculation {

  //@Inject @Secure
  @Inject @Normal
  private RandomGenerator randomGenerator;

  public void doSomething() {
    System.out.println(randomGenerator.randomInt());
  }
}

Dies funktioniert auch wenn man Constructor Injection verwendet.

@Named
public class Calculation {

  private RandomGenerator randomGenerator;

  @Inject
  public Calculation(@Normal RandomGenerator randomGenerator) {
    this.randomGenerator = randomGenerator;
  }

  public void doSomething() {
    System.out.println(randomGenerator.randomInt());
  }
}

Auf diese Art kann nun die Implementation umbenannt werden ohne dass das Injizieren fehlschlägt. Es muss natürlich immer eine Implementation vorhanden sein die mit @Secure oder @Normal annotiert wird. Wenn diese fehlt wird zur Laufzeit eine Exception aufgeworfen.

Spring 3.0.0 RC2

Am 13.11.2009 ist der zweite Releasecandidat des Spring Frameworks erschienen.

“Highlights” von diesem Release sind die vollständige Unterstützung von JSR-330 (“Dependency Injection for Java”).

Zusätzlich gibt es einen neuen ApplicationContext mit dem es möglich ist eine Springapplikation komplett ohne XML zu starten. AnnotationConfigApplicationContext scannt die angegebenen Packages nach Annotationen ab und registriert die gefunden Beans. Unterstützt werden die Spring eigenen Annotationen (@Component, @Service, …) wie auch die JSR-330 Annotationen (@Inject, @Named) sowie die “JavaConfig” Annotationen (@Configuration).

Weitere Aenderungen von RC2 findet man im Blogeintrag von Jürgen Hoeller.

Hier ein kleines Beispiel mit dem neuen AnnotationConfigApplicationContext.
Eine simple Komponente mit der Spring eigenen Annotation @Component

package ch.ralscha.test;
import org.springframework.stereotype.Component;

@Component
public class MyComponent {
  public void sayHello() { System.out.println("Hello World"); }
}

Im Constructor der Klasse AnnotationConfigApplicationContext übergibt man als Parameter eine Liste
von Packagenamen in welchen nach annotierten Klassen gescannt werden soll.

package ch.ralscha.test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class SpringTest {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("ch.ralscha.test");

    MyComponent component = ctx.getBean("myComponent", MyComponent.class);
    component.sayHello();
  }
}

Für dieses Beispiel wird kein XML benötigt.

jTDS und MS SQL Server mit dynamischem Port

Der Microsoft SQL Server lässt sich so einstellen das er sich beim Aufstarten selber einen Port zuweist auf dem man mit TCP/IP eine Verbindung aufbauen kann. Dies ist praktisch wenn zum Beispiel mehrere SQL Server Instanzen auf dem gleichen Server installiert werden müssen.

Wenn der jTDS Treiber für die JDBC Verbindung benutzt wird, dann lässt sich auch für diesen Fall sehr einfach eine Verbindung über TCP/IP aufbauen. Folgende Punkte sind dabei zu beachten.

Der “SQL Server Browser” Service muss gestartet sein. Dieser Service hört auf den Port 1434 UDP und gibt auf Anfrage den Port für eine bestimme SQL Server Instanz zurück.

Die Datenbank URL die man dem jTDS Treiber übergibt muss den Instanznamen enthalten. Eine URL könnte folgendermassen aussehen:

jdbc:jtds:sqlserver://localhost/MY_DB;instance=MSSQLSERVER

Weitere Informationen findet man im FAQ des jTDS Projektes.

Spring 3.0.0.RC1

Am 25.9.2009 ist der erste Releasekandidat für Spring 3.0.0 erschienen.

Das Changelog findet man hier: http://static.springsource.org/spring/docs/3.0.0.RC1/changelog.txt

Interessante Neuerungen sind zum Beispiel die Unterstützung für Hibernate 3.5 beta 1 und EclipseLink 2.0.0.M7. Weiter gibt es frühe Unterstützungen für JSR-330 (javax.inject) und JSR-303 (Bean Validation).

Die Beispiele aus den vorherigen Blogeinträgen dürften alle ohne Probleme laufen.

RestTemplate in Spring 3.0
XML-Object Mapping mit Spring und JAXB
XML-Object Mapping mit Spring, Castor und XStream
JMX mit Spring
JMS mit Spring
Emailversand mit Spring
Embedded DB mit Spring 3.0.0
Ansynchrone Methoden mit Spring 3.0.0
Scheduling mit Spring 3.0.0
Erste Schritte mit Spring 3.0.0 (M4)

Die Datei ivy.xml muss entsprechenden angepasst werden. Bei den Spring Artefakten muss nur die Revision 3.0.0.M4 durch 3.0.0.RC1 ersetzt werden. Hier am Beispiel von spring-core ersichtlich:

  ...
  <dependency org="org.springframework" name="spring-core" rev="3.0.0.RC1"/>
  ...