Spring AOP

Eingetragen bei: spring | 0

Der folgende Artikel beschäftigt sich mit Spring 3.x AOP, wobei Spring lediglich den Support für das eigentliche AOP-Framework AspectJ zur Verfügung stellt. Die Aspekt orientierte Programmierung – kurz AOP – ermöglicht es, die eigentliche Business-Logik einer Anwendung von querschnittlichen Aspekten zu trennen. Als typische Aspekte seien hier Logging- oder Security-Layer genannt. Statt nun die Aspekte mit der Business-Logik zu verweben, findet eine klare Kapselung statt, die es ermöglicht, die Aspekte autonom umzusetzen und anschließend explizit über die Business-Logik zu legen. So können öffentliche Methoden, ganze Projekte, einzelne Packages oder nur bestimmte Methoden eines Services mit Aspekten versehen werden.

Der Artikel erläutert wann und wie ein Aspekt in den Programmfluss eingreifen kann und wie die Aspekte mit der Business-Logik verwoben werden. Ersteres wird über Advices, Zweiteres per Pointcuts realisiert.

Bevor es an die Implementierung geht, müssen zunächst die entsprechenden Libraries eingebunden werden. Für den Spring AOP Support werden die Libraries core und context benötigt, die aktuell in der Version 3.1.0 verfügbar sind:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>

Wie bereits erwähnt handelt es sich bei Spring lediglich um den Wrapper um die AOP-Schicht. Für das eigentliche AOP Framework AspectJ werden folgende Libraries verwendet:

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>com.springsource.org.aspectj.runtime</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>com.springsource.org.aspectj.weaver</artifactId>
<version>1.7.0</version>
</dependency>

Um den AspectJ-Support für Spring zu aktivieren, muss folgende Zeile in der Spring-Konfiguration aufgenommen werden:

<aop:aspectj-autoproxy/>

Anschließend wird die Klasse MyService erstellt, welche die eigentliche Business-Logik beinhalten soll.

public class MyService {

public void doSomeStuff() {
// do some business logic
}
}

Dieser Service soll nun mit einem Aspekt versehen werden, der z.B. das Logging von der Business-Logik kapselt.

@Aspect
public class MyAspect {

@Before(„execution (* de.mz.MyService.doSomeStuff(..))“)
public void aspect() {
// do some logging
}
}

Nun muss sowohl der Service als auch der Aspekt als Spring-Bean definiert werden:

<bean id=“myAspect“ class=“de.mz.MyAspect“ />
<bean id=“myService“ class=“de.mz.MyService“ />

Wird nun die Methode doSomeStuff des MyService aufgerufen, wird nicht nur die eigentliche Methode, sondern auch der Aspekt ausgeführt. Die Reihenfolge wird hierbei über den Advice definiert. In obigen Beispiel wird dieser über die Before-Annotation festgelegt, so dass zuerst der Aspekt und erst anschließend die Methode des MyService ausgeführt wird.

Die folgenden Advices bieten die Möglichkeit Eingriffe im Programmfluss vorzunehmen:

  • Before – Der Aspekt wird vor der eigentlichen Methode ausgeführt.
  • Around – Die Methodenausführung findet im Aspekt selber statt. Es kann also vor und nach der Methodenausführung eingegriffen werden.
  • After – Der Aspekt wird erst nach der Methodenausführung aktiv.
  • AfterReturning – Dieser Aspekt wird ebenfalls erst nach der Methodenausführung aktiv, es kann allerdings zusätzlich auf den Rückgabewert zugegriffen werden.
  • AfterThrowing – Der Aspekt wird nur aktiv, sofern in der eigentlichen Methode ein Fehler auftritt.

Um Methoden mit Aspekten zu versehen, werden die bereits erwähnten Pointcuts verwendet. In obigem Beispiel wurde der Pointcut einfach in der Before-Annotation mit angegeben. Wesentlich eleganter ist es, Pointcuts separat zu definieren und im Advice lediglich auf sie zu verweisen. So können die Pointcuts mehrfach verwendet werden und sind beliebig kombinierbar:

@Pointcut(„execution (* de.mz.MyService.doSomeStuff(..))“)
private void pointcutForDoSomeStuff() {}

@Before(„pointcutForDoSomeStuff()“)
public void aspect() {
// do what the aspect has to do
}

Nun wird in der Pointcut-Annotation auf die doSomeStuff-Methode vom MyService verwiesen, wobei der Pointcut selber lediglich aus einer leeren Methoden besteht, deren Rückgabetyp void seien muss. Auch wenn es nicht zwingen notwendig ist, wird als Sichtbarkeit private festgelegt, damit der Pointcut nicht aufgerufen werden kann. Auf die Pointcut-Methode wird nun in der Before-Annotation verwiesen.

Es gibt verschiedene Möglichkeiten mit denen Pointcut-Ausdrücke auf entsprechende Java-Resourcen gematched werden können. Um auf Methoden zu matchen, wird der bereits verwendete Pointcut-Designator (PCD) execution verwendet. Es ist nicht zwingend notwendig, dass hier nur explizit auf eine Methode verwiesen wird, so kann z.B. auch auf alle öffentlichen Methoden des MyService verwiesen werden:

@Before(„execution(public * de.mz.MyService.*(..)))“)

Alternativ können auch alle öffentlichen Methoden – bzw. alle Methoden mit einer fest definierten Sichtbarkeit – mit einem Aspekt versehen werden:

@Before(„execution(public * *(..))“)

Will man Aspekte nicht für Methoden sondern auf einer bestimmten Package-Ebene definieren, wird statt dem execution Designator within verwendet:

@Pointcut(„within(de.mz.*)“)

Sollen zusätzlich die Sub-Packages mit Aspekten versehen werden, muss das Statement minimal angepasst werden:

@Pointcut(„within(de..*)“)

Weiterhin kann man alle Klassen, die ein bestimmtes Interface implementieren mit einem Aspekten versehen, indem man den Designator this verwendet. Würde der MyService nun das Interface IMyService implementieren, würde auch folgender Pointcut matchen:

@Pointcut(„this(de.mz.IMyService)“)

Es gibt noch viele weitere Möglichkeiten, so dass sich hier ein Blick in die Dokumentation allemal lohnt:

http://static.springsource.org/spring/docs/current/spring-framework-reference/html/aop.html

Wie bereits erwähnt, können Pointcuts auch kombiniert werden. Hierbei sind AND &&, OR || und NOT ! Verknüpfungen möglich.

Sollen die Methode doSomeStuff und doSomeOtherStuff denselben Aspekt erhalten wird zunächst ein weiterer Pointcut für die zweite Methode definiert:

@Pointcut(„execution (* de.mz.MyService.doSomeOtherStuff())“)
private void pointcutForSomeOtherStuff() {}

Anschließend können beide Pointcuts mit ODER Verknüpft werden, so dass der Aspekt ausgeführt wird, sobald eine der Methoden aufgerufen wird:

@Pointcut(„pointcutForDoSomeStuff() || pointcutForSomeOtherStuff()“)
private void allPointcuts() {}

Weiterhin kann es nützlich sein im Aspekt auf die Übergabeparameter der aufgerufenen Methode zugreifen zu können. Hierzu wird die Methode doSomeStuff um zwei Übergabeparameter vom Typ String erweitert:

public void doSomeStuff(String para1, String para2) {
// do some business logic
}

Der bestehende Pointcut wird mittels UND mit dem Designator args verknüpft, welcher Zugriff auf die Parameter gewährt. Diese stehen dem Aspekt dann als eigene Übergabeparameter zur Verfügung:

@Before(„pointcutForDoSomeStuff() && args(para1,para2)“)
public void printParams(String para1, String para2) {
// nice aspect with access to parameters
}

Interessiert einen nur der erste Parameter, sieht das Statement so aus:

@Before(„pointcutForDoSomeStuff() && args(para1,..)“)

Für Zugriff auf den zweiten Parameter muss folgende Modifizierung vorgenommen werden:

@Before(„pointcutForDoSomeStuff() && args(..,para2)“)

Da aus Programmierer Sicht kein Unterschied zwischen Before- und After-Advice existiert, beschäftigt sich das folgende Beispiel mit dem Around-Advice. Hier ergeben sich interessante Möglichkeiten, da der Aspekt Zugriff auf den JoinPoint erhält und für dessen Ausführung zuständig ist. Bei dem JoinPoint handelt es sich um die mit einem Aspekt versehene Methode. Dadurch das die Ausführung dem Aspekt obliegt, kann im Fehlerfall eingegriffen und z.B. ein Retry-Mechanismus implementiert werden. Die Exception kann gecatched und der Methoden-Aufruf beliebig oft wiederholt werden:

@Around(„pointcutForDoSomeStuff“)
public Object retryexecution(ProceedingJoinPoint joinPoint) throws Throwable {
Exception exception = null;
int i = 0;
while (i < MAX_RETRY) {
try {
return joinPoint.proceed();
} catch (MyException e) {
exception = e;
}
i++;
}
throw exception;
}

Grade beim Aufruf von externen Services in verteilten Systemen kann so ein Retry-Mechanismus Sinn machen. Geht der erste Aufruf noch auf einen defekten Service, so kann der zweite erfolgreich die gewünschte Arbeit verrichten. Der Benutzer bekommt hierbei nicht einmal mit, dass ein Fehlverhalten aufgetreten ist.

Wer bereits mit Spring 2.x AOP gearbeitet hat, dem wird das Handling der obigen Beispiele vertraut vorkommen. Tatsächlich hat sich die Spring-API im AOP-Bereich seit der letzten Spring Version nicht geändert. Aufgrund der komfortablen API ist dies aber auch nicht Notwendig.

NoSQL Datenbanken im Allgemeinen und MongoDB im Speziellen

Eingetragen bei: Java, NoSQL | 0

Waren Relationale-Datenbanken lange Zeit das Speichermedium der Wahl, so sind in den letzten Jahren NoSQL-Datenbanken (Not only SQL) populär geworden. Diese können und sollen die bestehenden Relationalen-Datenbanken nicht ersetzten, da sie ganz andere Ansätze der Datenspeicherung verfolgen. Es kann aber durchaus Sinn machen, Relationale- und NoSQL-Datenbanken zu kombinieren, um das jeweilige Verhalten optimal zu nutzen. Nicht nur zwischen Relationalen- und NoSQL-Datenbanken gibt es Unterschiede, auch die Ansätze der NoSQL-Datenbanken weichen erheblich voneinander ab. Je nach Einsatzbereich, sind sie auf ihre Aufgabe hin optimiert. Grob können NoSQL-Datenbanken in die folgenden vier Kategorien aufgeteilt werden:

  • Key/Value basierte Datenbanken: Jedem Wert wird ein eindeutiger Schlüssel zugewiesen, der Wert selber wird von der Datenbank nicht interpretiert.
  • Wide Column Store: Hier sind die Spalten einer Tabelle nicht fest definiert. Muss für einen Datensatz eine zusätzliche Information gespeichert werden, kann diese hinzugefügt werden, ohne dass alle bestehenden Datensätze um einen NULL-Wert ergänzt werden. Letzteres wäre beim relationalen Datenmodell der Fall gewesen.
  • Dokumentenorientierte Datenbanken: Es werden ganze Dokumente gespeichert, meistens handelt es sich um XML- oder JSON-Dokumente.
  • Graphen Datenbanken: Diese Datenbanken enthalten Knoten und deren Beziehungen zueinander. Sie sind auf das schnelle Durchlaufen von Graphen ausgelegt.

Einige der NoSQL-Datenbanken sind auf große Mengen an Daten ausgerichtet, daher spielt hier die horizontale Skalierbarkeit eine wichtige Rolle. Bei verteilten Datenbanksystemen kommt nun das CAP-Theorem zum Einsatz, welches besagt, dass bei verteilten Computer Systemen maximal zwei der folgenden drei Eigenschaften erfüllt werden können:

  • Konsistenz (Consistency): Alle Clients sehen zum gleichen Zeitpunkt die gleichen Daten.
  • Verfügbarkeit (Availability): Antwortzeit in der ein Request beantwortet wird.
  • Partitionstoleranz (Partition tolerance): Das System arbeitet weiter, auch wenn einzelne Nachrichten verloren gehen oder einzelne System-Komponenten fehlerhaft arbeiten.

Betrachten wir nun die MongoDB, eine der vielen NoSQL-Datenbanken, genauer. Der Name steht für humongous database – gigantische Datenbank – ist dokumentenorientiert und arbeitet mit JSON ähnlichen Dokumenten. An Anlehnung an JSON heißt das Format BSON – Binary JSON – enthält aber zusätzliche Datentypen wie Date und BinData. Will man die MongoDB nach dem CAP-Theorem einsortieren, so handelt es sich um ein CP-System. Sie hat also Probleme mit Hochverfügbarkeit, während die Daten über verschiedene Knoten Konsistent gehalten werden können.

MongoDB ist aktuell in der Version 2.0.4 verfügbar und kann unter
http://www.mongodb.org/ bezogen werden. Um die MongoDB anschließend unter Windows als Dienst zu registrieren, muss mongod.exe mit folgenden Parametern ausgeführt werden:

mongod.exe –logpath C:mongologslogfilename.log –logappend –dbpath C:mongodata –install

Auch wenn es erstaunen hervorruft, aber unter Windows ist die Angabe der Log-Datei tatsächlich zwingend notwendig.

Unter Linux (Ubuntu) ist es einfacher, hier kann die MongoDB direkt aus der Paketverwaltung installiert werden. MongoDB wird direkt als Service gestartet, die Konfigurations-Datei enthält unter anderem folgende Einträge:

dbpath=/var/lib/mongodb
logpath=/var/log/mongodb/mongodb.log

Die Datei selber ist unter /etc/mongodb.conf zu finden.

Die MongoDB bietet support für diverse Programmiersprachen, u.a für

  • Java
  • C
  • C++
  • .NET
  • PHP

Der aktuelle MongoDB Java Treiber ist in Version 2.7.3 verfügbar und kann bequem als Maven Dependency geladen werden:

<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>2.7.3</version>
</dependency>

Um eine Verbindung zur MongoDB aufzubauen, sind lediglich folgende zwei Zeilen nötig:

Mongo mongo = new Mongo();
DB db = mongo.getDB(„myDB“);

Sofern die Datenbank myDB in der MongoDB noch nicht existiert, wird diese automatisch erstellt. Da der Java MongoDB Treiber thread-safe ist, kann beispielsweise in einer Web Anwendung eine einzige Mongo Instanz für die Abarbeitung alle Requests betrieben werden. Sofern nicht anders konfiguriert beinhaltet das Mongo Objekt einen internen Pool von 10 Datenbank-Connections. Bei jeder Anfrage wird eine Connection über den Pool aufgebaut, die Abfrage gesendet und anschließend die Connection wieder abgebaut.

Will man nun seinen ersten Eintrag in der MongoDB speichern, wird ein BasicDBObject erstellt, welchem die Attribute als Key/Value Paare übergeben werden.

BasicDBObject person = new BasicDBObject();
person.put(„forename“, „Max“);
person.put(„lastname“, „Mustermann“);

DBCollection coll = db.getCollection(„persons“);
coll.insert(person);

Anschließend holt man sich über die Datenbank die Collection mit dem Namen persons. In einem Relationalen-Datenbankmodell würde man nicht von Collections, sondern von Datenbank-Tabellen reden. Hat man nun die Collection, kann das bereits generierte BasicDBObject gespeichert werden. Sollte noch keine Collection mit dem Namen persons existieren, wird auch diese automatisch erzeugt.

Will man die Einträge einer Collection erfragen, stehen verschiedene find-Methoden zur Verfügung. Um alle Einträge zu erhalten, wird die find-Methode ohne weitere Parameter – und damit ohne Einschränkungen – aufgerufen:

DBCollection collection = db.getCollection(„persons“);
DBCursor cursor = collection.find();
while (cursor.hasNext()) {
DBObject dbObject = cursor.next();
String forename = (String) dbObject.get(„forename“);
String lastname = (String) dbObject.get(„lastname“);
Person person = new Person(forename, lastname);
}

Die find-Methoden liefern einen DBCursor, mit dem über die Elemente der Collection persons iteriert werden kann. Sind die Parameter des DBObjects bekannt, können diese  – forename und lastname – direkt erfragt werden.

Will man nun die Suche einschränken, wird das gleiche BasicDBObject verwendet, welches schon beim Erzeugen des Eintrags zum Zuge kam. Gewünschte Einschränkungen werden auch hier als Key/Value Paare definiert:

BasicDBObject query = new BasicDBObject();
query.put(„forename“, „Max“);
DBCursor cursor = collection.find(query);

In obigem Beispiel waren die Eigenschaften der Collection persons bekannt, so dass diese direkt über ihren Schlüssel erfragt werden konnten. Da die Eigenschaften einer Collection nicht fest definiert sind und im Laufe der Zeit zusätzliche Key/Value Paare hinzugefügt werden können, müssen auch alle Schlüssel eines DBObjects erfragt werden können:

Set<String> keys = dbObject.keySet();
for (String key : keys) {
Object property = dbObject.get(key);
}

Auch über alle Collections einer Datenbank kann bequem per Java Mongo Treiber API iteriert werden:

Set<String> collectionNames = db.getCollectionNames();
for (String name : collectionNames) {
DBCollection collection = db.getCollection(name);
}

Die Größe von BSON Objekten, die in der MongoDB gespeichert werden können, sind beschränkt. In älteren MongoDB Version liegt die Begrenzung bei 4 MB, ab Versionen 1.7 bei 16 MB. Um dennoch größere Dokumente speichern zu können, wurde die GridFS-Spezifikation zum Speichern von großen Objekten eingeführt. Diese werden über zwei Collections realisiert. In der Collection files sind die Meta-Informationen zu den Daten enthalten, wie Dateiname und Content-Type. Die Objekt-Daten werden zu chunks von ca. 256 k Größe gesplittet und in der gleichnamigen Collection chunks abgelegt. D.h. jede Datei hat einen Eintrag in files und mindestens einen Eintrag in chunks.

Die Speicherung von Dateien über GridFS ist in Java schnell umgesetzt:

InputStream inputStream = new FileInputStream(new File(„somepic.jpg“));
GridFS storeGridFS = new GridFS(db);
GridFSInputFile gridFSInputFile = storeGridFS.createFile(inputStream);
gridFSInputFile.setFilename(„somepic.jpg“);
gridFSInputFile.setContentType(„image/jpeg“);
gridFSInputFile.save();

Über die erzeugte GridFS-Instanz erhält man Zugriff auf das Standard-GridFS der Datenbank. Alternativ kann zusätzlich ein Name angegeben werden, um so ein neues GridFS zu erzeugen. Nun wird der Inhalt der Datei per InputStream der createFile-Methode übergeben. Hier erhält man einen GridFSInputFile, dem Meta-Informationen wie Dateiname und Content-Type übergeben werden können. Diese Informationen werden nach dem Aufruf der save-Methode in der oben erwähnten Collection files persistiert. Die Datei selber wird in chunks aufgeteilt und in eben dieser Collection gespeichert.

Auch das Auslesen der Datei ist schnell implementiert:

GridFS loadedGridFS = new GridFS(db);
List<GridFSDBFile> gridFSDBFiles = loadedGridFS.find(„somepic.jpg“);
GridFSDBFile gridFSDBFile = gridFSDBFiles.get(0);
InputStream in = gridFSDBFile.getInputStream();

Sind Meta-Informationen über die Datei bekannt, wie hier der Dateiname, so kann der GridFSDBFile über das Standard-GridFS geladen werden. Nun stehen die Daten der Datei als InputStream zur Verfügung.

Natürlich kann man sich auch alle Inhalte der file-Collection ausgeben lassen, um auf die entsprechenden Daten per InputStream zugreifen:

GridFS loadedGridFS = new GridFS(db);
DBCursor fileCursor = loadedGridFS.getFileList();
while (fileCursor.hasNext()) {
DBObject fileObject = fileCursor.next();
GridFSDBFile file = loadedGridFS.find((ObjectId) fileObject.get(„_id“));
InputStream in = file.getInputStream();
}

Nach erfolgreicher Arbeit sollte man nicht vergessen die Verbindung zur MongoDB zu schließen und damit die Connection zurück in den Pool zu legen:

mongo.close();