Spring AOP

Veröffentlicht in: 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.

Testgetriebene Entwicklung mit JUnit, EasyMock und Spring

Veröffentlicht in: Java, test-driven development | 0

Die testgetriebene Entwicklung – test-driven development (TDD) – wird häufig in der agilen Softwareentwicklung eingesetzt. Hierbei werden zunächst die Testfälle konzeptioniert und erst anschließend die gewünschte Funktionalität umgesetzt. Aufgrund fehlender Umsetzung werden daher die Testfälle zunächst zwangsläufig fehlschlagen. Erst mit sukzessiver Funktions-Implementierung werden die Testfälle nach und nach erfolgreich durchlaufen.

Am einfachsten zu Testen sind Methoden, die für unterschiedliche Übergabe-Parameter entsprechende Rückgabe-Werte liefern wie z.B. statische Utility-Methoden. Um diese Methoden zu testen wird JUnit verwendet, welches unter http://www.junit.org/ in der aktuellen Version 4.10 zum Download bereit liegt. Alternativ kann man sich JUnit auch als Maven Dependency laden:

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>

Das Testen einer Utility-Methode mit JUnit sieht dann folgendermaßen aus:

assertTrue(MyUtility.returnTrue(„parameter“));

Sofern die zu testende Methode Aufgaben an weitere Klassen delegiert, ist es nicht mehr möglich ausschließlich JUnit Tests zu verwenden, ohne eine vollständige Anwendungs-Umgebung zu initialisieren. Stattdessen müssen zusätzlich alle verwendeten Klassen mit initialisiert und getestet werden.

Hierbei handelt es sich dann nicht mehr um Modultests, sondern um Integrationstests, die definitiv wichtig sind und ihre Berechtigung haben, bei der testgetriebenen Entwicklung aber nicht im Vordergrund stehen. Hier soll nur das zu entwickelnde Modul getestet werden.

Daher werden bei der testgetriebenen Entwicklung Mocking-Frameworks verwendet, damit alle Klassen die in der zu testenden Methode verwendet werden, durch sogenannte Mock-Objekte ersetzt werden können. Diese Mocks bieten die Möglichkeit zu testen, ob die erwarteten Methode-Aufrufe mit den definierten Übergabe-Parametern erfolgen. Findet der Aufruf nicht statt oder ein nicht erwarteter Aufruf, so schlägt der Testfall fehl. Die Funktionalität der gemockten Objekte wird also nicht mit getestet, sondern kann in separaten Testfällen geprüft werden.

Als Mocking-Framework wird im folgenden Beispiel EasyMock verwendet, welches unter http://easymock.org/ in der aktuellen Version 3.1 verfügbar ist. Auch EasyMock ist als Maven Dependency erhältlich:

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymockclassextension</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>

Die erste Library wird zum Mocken von Interfaces benötigt, was als best practice anzusehen ist. Ist es dennoch mal nötig eine Klasse zu mocken, weil man z.B. bestehenden Code nachträglich testen möchte, kann die EasyMock-Classextension verwendet werden.

Kommen wir nun zu einem kleinen Beispiel. Es wird ein UserService getestet der einen User erhält und diesen mittels eines UserDAOs speichern soll. Hierbei wird der UserDAO gemockt und es soll überprüft werden, ob die save-Methode des DAOs auch wirklich aufgerufen wird.

Auch wenn bei der testgetriebenen Entwicklung zuerst der Testfall erstellt werden sollte, folgt zum besseren Verständnis zunächst der UserService und erst anschließend die Testfälle:

public class UserService {

private UserDAO userDAO;

public User save(User user) {
return userDAO.save(user);
}

public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
}

Zuallererst wird ein Setup initialisiert, in welchem der DAO gemockt und die erwarteten Methoden-Aufrufe definiert werden. Die Annotation @Before besagt hierbei, dass das Setup vor der Ausführung jedes einzelnen Testfalls in dieser Test-Klasse ausgeführt werden soll.

@Before
public void setUp() {
name = „userName“;
user = new User(name);
userDAO = createMock(UserDAO.class);
expect(userDAO.save(user)).andReturn(user); // (1)
replay(userDAO);

userService = new UserService();
userService.setUserDAO(userDAO);
}

Wie bereits erwähnt wird der UserDAO gemockt und die Erwartung ausgesprochen, dass die save-Methode mit dem gegebenen User aufgerufen wird. Bei der expect-Methode handelt es sich um eine statische Methode von EasyMock, die verwendet wird, wenn der erwartete Methode-Aufruf einen Return-Value besitzt. Über die andReturn-Methode kann anschließend der Rückgabe-Wert definiert werden, den das Mock-Objekt zurückgeben soll.

Hätte die save-Methode keinen Rückgabe-Wert, sähe die Stelle (1) folgendermaßen aus:

userDAO.save(user);

Der expect-Aufruf würde also einfach weggelassen. Der replay-Aufruf in der darauf folgenden Zeile besagt, dass keine weiteren Aufrufe auf dem UserDAO-Mock erwartet werden und nun nur noch die Aufrufe folgen, die bei Test-Ausführung getätigt werden.

Nun muss der UserService noch den UserDAO-Mock gesetzt bekommen und wir können zum eigentlich Test kommen. Hierbei sollte man sich nicht wundern, das Setup ist meist deutlich länger als der eigentliche Test.

@Test
public void shouldSaveUser() {
User save = userService.save(user);
verify(userDAO);
assertNotNull(save);
assertEquals(„userName“, save.getName());
}

Hier wird die save-Methode des UserService aufgerufen und anschließend die verify-Methode von EasyMock aufgerufen. Damit wird überprüft ob die zuvor definierten Mock-Aufrufe auch wirklich getätigt werden oder nicht. Anschließend kann der Rückgabe-Wert wie gehabt mit JUnit überprüft werden.

Um EasyMock etwas genauer zu verstehen modifizieren wir nun die Zeile (1) der Setup-Methode:

expect(userDAO.save(new User(„userName“))).andReturn(user);

In der Setup-Methode wird dem UserDAO-Mock also ein anderer User übergeben, als dem UserService im Test, lediglich der Namen ist gleich. Da nur der Name auf Gleichheit überprüft wird, läuft der Testfall wie gehabt durch. Fügen wir nun dem Testfall noch folgende Zeile hinzu:

assertEquals(save, user);

Hierauf hin wird der Testfall fehlschlagen. Grund dafür ist, dass EasyMock die equals-Methode der User-Klasse überprüft, um zu Testen ob der Übergabe-Parameter gleich ist. Sofern die equals-Methode nicht überschrieben wurde, wird die equals-Methode von Object verwendet, die auf gleiche Referenz testet. Da hier nun zwei unterschiedliche Objekte mit lediglich den gleichen Property-Werten verwendet werden, schlägt der Testfall fehl. Sobald allerdings die equals-Methode von der User-Klasse überschrieben wird und hierbei auf Namens-Gleichheit getestet wird, ist der Test-Durchlauf wieder erfolgreich.

Dies Verhalten ist auch der Grund, warum ich auf einen weiteren wichtigen Bestandteil von EasyMock kommen möchte, das Capture.

Stellen wir uns vor, der UserService reicht das User-Objekt nicht direkt zum UserDAO durch, sondern erstellt aufgrund des Users ein UserEntity, welches vom UserDAO konsumiert wird. Die Properties des Users sind hierbei identisch mit denen des UserEntities. Die Implementierung sieht also so aus:

public User save(User user) {
UserEntity userEntity = new UserEntity(user.getName());
UserEntity entity = userDAO.save(userEntity);
User result = new User(entity.getName());
return result;
}

Sofern das UserEntity viele Properties beinhaltet, müßte für den Testfall also ein exakt gleiches UserEntity erstellt werden, damit das UserEntity im Testfall und das im UserService erzeugte von EasyMock als gleich anerkannt wird. Sofern die equals-Methode nicht überschrieben werden dürfte, würde der Testfall sogar immer fehlschlagen, da es sich zwangsläufig um zwei unterschiedliche Referenzen handeln würde. Aber auch so ein Fall lässt sich problemlos mit EasyMock abbilden.

Hierzu muss lediglich der übergebene User durch ein Capture ersetzt werden:

captureUser = new Capture<UserEntity>();
userDAO = createMock(UserDAO.class);
expect(userDAO.save(capture(captureUser))).andReturn(userEntity);
replay(userDAO);

Es wird ein Capture Objekt erzeugt, welches nicht direkt in die save-Methode des UserDAOs gegeben wird. Stattdessen wird hier die EasyMock-Methode capture aufgerufen. Nun kann im Test über das Capture auf das dem Mock übergebene Objekt zugegriffen und die Werte überprüft werden:

assertNotNull(captureUser.getValue());
UserEntity userEntity = captureUser.getValue();
assertEquals(„userName“, userEntity.getName());

Mit diesem Funktionsumfang von EasyMock lassen sich bereits die meisten Test-Szenarien abdecken. Ab und an wird man in spezial Fällen Lösungen in der EasyMock-Dokumentation suchen müssen, für die meisten Anwendungsfälle dürfte es aber genügen.

Nun noch ein kleiner Exkurs über die Modultest hinaus zu den Integrationstests. Sofern man Spring verwendet kann es sehr nützlich sein, für Testfälle einen Spring-Context hochzuziehen. Folgende Spring-Library ermöglicht dies:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.0.6.RELEASE</version>
<scope>test</scope>
</dependency>

Nun legt man den zu testenden Service an und definiert diesen in der Spring-XML Konfiguration, welche sich im Classpath befinden sollte:

<version=“1.0″ encoding=“UTF-8″?>
<beans>
<context:annotation-config/>
<bean id=“myService“ class=“MyService/>
</beans>

Nun kann man die XML-Konfiguration über die Annotation ContextConfiguration angeben und muss anschließend die Runner Klasse definieren. Da in der Spring-Konfiguration Annotations aktiviert wurden, kann nun der Service direkt in den Testfall per Autowire-Annotation injected werden:

@ContextConfiguration(locations = { „/test.xml“ })
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringContextTest {

@Autowired
private MyService myService;

@Test
public void shouldExecuteMyService() {
assertNotNull(myService);
assertTrue(myService.doWhatYouHaveTodo());
}
}

Über die testgetriebene Entwicklung wird immer wieder gesagt, dass sie zu langsam und damit zu teuer ist. Dies konnte ich bislang nicht feststellen. Sicherlich ist es zunächst mehr Aufwand testgetrieben zu Entwickeln, aber die Einarbeitung in jedes neue Framework kostet zunächst einmal Zeit und bringt es mit sich, sich bei neuen Gegebenheiten erneut mit der API und deren Dokumentation zu beschäftigen. Der Zeitaufwand der beim Erstellen der Testfälle selber benötigt wird, ist überschaubar und wird durch die direkt bemerkten Flüchtigkeitsfehler wieder wett gemacht. Es muss nicht umständlich ein Server gestartet werden, nur um dann doch wieder ein Fehlverhalten festzustellen. Es ist grundsätzlich schneller einen Testfall auszuführen, als bei einer Software manuell einen ganz bestimmten Zustand zu testen.