Aspect Oriented Programming met Spring Boot

Aspects? Join Points? Advices? Pointcuts? Een aantal termen die velen van ons misschien wel gehoord hebben maar toch te vaag en vreemd zijn om er eens mee aan de slag te gaan. En dat is zonde; want Spring maakt beginnen met Aspect Oriented Programming (AOP) erg eenvoudig. In dit artikel zal ik proberen deze termen duidelijk te maken aan de hand van een aantal concrete oplossingen.

Niels Dommerholt

Maar laten we eerst een stap terug zetten. Wat is AOP? Welk probleem lost het voor ons op? Vrijwel elk stuk software bevat herhalende functionaliteit, oftewel “cross-cutting concerns”. cross-cutting concerns zijn ‘dingen’ die op meerdere plekken in je code voorkomen. Voorbeelden zijn logging, het verzamelen van metrics en het beveiligen van REST endpoints. Spring heeft hier zelf al een groot aantal libraries voor beschikbaar, maar Spring biedt je ook de mogelijkheid om naar eigen inzicht (bijvoorbeeld) AOP toe te passen.

Een andere richting die je zou kunnen overwegen is een OO oplossing; dus classes een Base class met deze functionaliteit laten extenden. Dit is een veelvoorkomende valkuil op zich; je zit dan al snel aan monsterlijk grote base classes met een hoop uitzonderingen omdat misschien bijvoorbeeld een RestController en een Service dit soort gedrag delen terwijl ander gedrag alleen gedeeld wordt door Services en Repositories.

Aspect Oriented Programming lost dit probleem voor je op en stelt je in staat om op willekeurige plekken in je software dit soort cross-cutting concerns af te handelen.

 

Aspect Oriented Programming

In het project waar ik op dit moment op werk gebruiken we Spring AOP om een aantal van onze cross-cutting concerns af te handelen. Het is een microservice architectuur draaiende in een Kubernetes cluster. De cross-cutting concerns die wij afhandelen zijn het loggen van de metrics van service calls tussen microservices (hoe lang duurt het voordat service X antwoord heeft van service Y) en bijhouden van de health status op basis van of de database reageert: als een service niet met de database kan praten gaat de service op rood.

 

Om dit schematisch weer te geven:

 

Zoals je ziet is dit een standaard REST service: het heeft een aantal controllers, deze praten met een aantal service classes (business logic) en deze praten op hun beurt weer met onderliggende JPA repositories die de persistence regelen. Dwars door al deze lagen lopen zaken die we eigenlijk overal geregeld willen hebben; logging, health, metrics, etc.

In dit artikel zal ik een aantal code voorbeelden aanhalen. De voorbeelden zijn onderdeel van een simpele “Todo” rest service waarvan de sourcecode hier te vinden is: https://github.com/nielsutrecht/spring-boot-aop. Het bevat een drietal voorbeelden van AOP implementaties.

 

Terminologie

Binnen AOP worden een aantal vrij unieke termen gebruikt. De Spring AOP documentatie legt ze vrij duidelijk uit en de belangrijkste van deze termen zijn.

Een aspect is een verzameling code die een of meer advices implementeert. Een aspect is een class met de @Aspect annotation. Het is over het algemeen een verzameling soortgelijke advices. Een LoggingAspect class kan dus bijvoorbeeld verschillende logging advices met elk specifieke implementaties bevatten: een logging advice voor controller methods zal misschien net iets anders werken dan eentje voor repositories bijvoorbeeld.

Een advice is dus een stuk code dat wordt toegepast op een join point. Een join point is de plek in je code waar de code toegevoegd wordt (binnen spring AOP is dat altijd een methode, met AspectJ kan dat vrijwel overal zijn). Het selecteren van een join point wordt gedaan door middel van een pointcut expressie. Een pointcut selecteert de plek in de code (de join point) waar een advice (implementatie van je cross-cutting concern) toegevoegd wordt.

 

Pointcut expressies

Pointcuts zijn iets waar ik nog wat dieper op in wil gaan; hier worden binnen Spring de enorm krachtige AspectJ expressies voor gebruikt. Een erg simpele vorm van zo’n expressie is bijvoorbeeld: execution(public * *(..))

Deze expressie selecteert alle public methods. Hiermee ‘hang’ je dus je advice waarvoor je deze pointcut gedefinieerd hebt aan iedere public method ongeacht van de return type, naam of parameters.

Nog een voorbeeld: execution(public void set*(..))

Deze pointcut selecteert iedere methode die public is, void als return type heeft en waarvan de method naam begint met set. Oftewel; alle public setters.

Er zijn veel meer expressies dan alleen execution expressions. Een andere erg krachtige is de ‘annotation’ expressie; deze laat je join points selecteren met een bepaalde annotation. Een voorbeeld: @annotation(org.springframework.web.bind.annotation.RequestMapping)

Deze pointcut selecteert join points met de Spring RequestMapping annotation; ideaal om aspects aan REST controllers te hangen! Ook kunnen pointcuts in een expressie gecombineerd worden: @annotation(org.springframework.web.bind.annotation.RequestMapping) && execution(public * admin*(..))

Deze selecteert dus alle methods met de RequestMapping annotation die public zijn en waarvan de naam begint met ‘admin’. Ideaal voor een security aspect dus.

Dit zijn slechts een paar voorbeelden. De expressie taal is enorm flexibel en krachtig, de AspectJ documentatie bevat veel meer voorbeelden. Hou er wel rekening mee dat Spring niet alle pointcut expressies ondersteunt; call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, and @withincode kent Spring niet. Als je deze toch gebruikt binnen Spring AOP krijg je at runtime een IllegalArgumentException.

 

Spring AOP versus AspectJ

Zoal ik al meldde: Spring gebruikt de AspectJ annotations en expressies maar ondersteunt slechts een relatief beperkt (maar voor de meeste toepassingen voldoende) aantal van de mogelijkheden van AspectJ. Spring is daarentegen een stuk simpeler in het gebruik.

AspectJ injecteert code in je classes, dit wordt ‘weaving’ genoemd. Omdat het met een eigen compiler werkt (AJC) kan het op vrijwel iedere plek code injecteren. Dit kan je doen in je build of at runtime door middel van ‘load time weaving’, mits je een classloader gebruikt die dit ondersteunt. Je kunt bijvoorbeeld calls naar System.out vervangen voor logging calls.

Spring AOP is veel beperkter; Spring maakt voor classes waarop aspects van toepassing zijn proxy classes aan. Dit betekent dat Spring alleen aspects toe kan passen op components en binnen die components bovendien alleen op methodes. Spring genereert deze proxy classes op het moment dat de application context opgetuigd wordt.

Hoewel Spring beperkter is gebruikt het wel de AspectJ annotations en expressies. Het is eenvoudig te starten met Spring AOP. Mocht dan blijken dat de mogelijkheden van Spring AOP te beperkt zijn is een migratie naar AspectJ erg simpel.

 

Spring AOP

Getting started

Nou, hoe eenvoudig is het toevoegen van Spring AOP aan een bestaand Spring Boot project? Erg eenvoudig. We beginnen met het toevoegen van de dependency aan de pom.xml (of build.gradle):

 

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-aop</artifactId>

    <version>${spring.boot.version}</version>

</dependency>

 

Het enige wat ons nog rest is een annotation toevoegen:

 

@Configuration

@EnableAspectJAutoProxy

public class ApplicationConfiguration {

}

 

That’s it! Spring gaat nu zelf op zoek naar Aspects en deze indien van toepassing zijn de Advices toepassen op Join Points geselecteerd in Pointcuts.

 

Voorbeeld: loggen doorlooptijd method call

Laten we beginnen met een eenvoudig voorbeeld: het loggen van de doorlooptijd van onze repository methodes. We willen graag van elke call bijhouden hoe lang onze database erover doet om met een resultaat te komen. We hebben een TodoRepository met een enkele get methode:

 

public List<TodoList> get(final UUID userId) {

    return db.computeIfAbsent(userId, u -> new ArrayList<>());

}

 

Zonder AOP zou de methode met extra logging er ongeveer zo uitzien:

 

public List<TodoList> get(final UUID userId) {

  long start = System.currrentTimeMillis();

  List<TodoList> list = db.computeIfAbsent(userId, u -> new ArrayList<>());

  long end = System.currrentTimeMillis();

  log.info(“UserService.login took {} ms”, end – start);

  return list;

}

 

We gaan van een methode met een enkele regel naar eentje met 5 regels (lege regels niet meegerekend) toe. In een applicatie met 100 van dergelijke calls heb ik dan opeens zo’n 400 extra regels code. En dat voor een enkele cross-cutting concern! Gelukkig kan dat met AOP beter. Voor deze Aspect ga ik een zogenaamder ‘marker’ annotation maken. Dit is een lege annotation puur voor het gebruik in pointcut expressies:

 

public @interface Timed {

}

 

Dit is natuurlijk niet noodzakelijk; je kan prima alle public methods in alle *Repository classes selecteren. Maar een groot voordeel van marker annotations is dat ze je code erg expliciet maken; het is duidelijk te zien welke methodes wel en niet getimed worden. We voegen de annotation toe aan de method:

 

@Timed

public List<TodoList> get(final UUID userId) {

    return db.computeIfAbsent(userId, u -> new ArrayList<>());

}

 

De volgende stap is het maken van de aspect zelf:

 

@Aspect

@Component

@Slf4j

public class TimeLogAspect {

    @Around(“@annotation(com.nibado.example.springaop.aspects.Timed) && execution(public * *(..))”)

    public Object time(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        long start = System.currentTimeMillis();

        Object value;

        try {

            value = proceedingJoinPoint.proceed();

        } catch (Throwable throwable) {

            throw throwable;

        } finally {

            long duration = System.currentTimeMillis() – start;

            log.info(

                    “{}.{} took {} ms”,

                    proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName(),

                    proceedingJoinPoint.getSignature().getName(),

                    duration);

        }

        return value;

    }

}

 

Ja, dat is best een hoop code. Maar let wel; dit hoef je maar een keer te doen voor een cross-cutting concern. Wij hebben onze aspects in een library zitten die simpelweg in elke service gebruikt wordt.

Maar, wat doet deze code precies? Ik heb een TimeLogAspect class gemaakt, met hierop de @Aspect en @Component (nodig voor Spring). Daarbinnen heb ik een enkele Advice (public Object time()) gedefinieerd. De @Around annotation instrueert Spring AOP dat ik code ‘om de Join Point heen’ wil wrappen (dus ik heb controle over het begin en het einde van de methode).

De join point is hier de get method in de TodoRepository class. De ProceedingJoinPoint instantie die wij hier binnen krijgen is een referentie naar deze methode; we kunnen de naam van de methode, de class, parameters en annotations hiermee benaderen. Let wel; de onderliggende ‘echte’ methode is nog niet aangeroepen op dit moment, dit gebeurt hier:

 

value = proceedingJoinPoint.proceed();

 

Dus alles voor deze regel treedt op ‘voor’ de methode aanroep, de rest (dus o.a. het loggen van de tijd) erna. Zoals al gezegd; de @Around annotation instrueert Spring dat wij een ‘around’ advice type willen. Het is niet het enige type advice, de verschillende types zijn:

 

Before wordt uitgevoerd voor de pointcut.
After returning wordt alleen uitgevoerd als de join point normaal returned (dus er geen exception gegooid wordt)
After throwing wordt alleen uitgevoerd als de join point een throwable gooit.
After wordt altijd uitgevoerd ongeacht of er een throwable gegooid wordt of niet
Around de meest krachtige; een combinatie van Before en After.

 

 

De ‘value’ van de @Around annotation is @annotation(com.nibado.example.springaop.aspects.Timed) && execution(public * *(..)). Oftewel; ik selecteer als join points alle public methods met de @Timed annotation. Wederom; je bent hier volledig vrij in en kunt in plaats hiervan ook op basis van method name, class name of package naam selecteren bijvoorbeeld.

Als we nu de applicatie starten kunnen we met curl (of een andere client) requests uitvoeren:

$ curl -H “user-id: 00000000-0000-0000-0000-000000000000” http://127.0.0.1:8080/todo/me

{“todoLists”:[]}

(de UUID header is onze ‘autorisatie’), in de logs zal de volgende regel verschijnen:

 

2017-06-09 11:01:05 INFO  c.n.e.s.aspects.TimeLogAspect – TodoRepository.get took 6 ms

 

Cool! Maar wat gebeurt er nu onder water? Als we de applicatie starten gaat Spring component scannen en vindt daarbij onze Aspect. Deze aspect wordt toegepast op de bij Spring bekende componenten en als Spring ziet dat daaruit een Join point ontstaat wordt er voor die componenten een proxy gemaakt. Als je in de code een breakpoint zet zul je zien dat in de TodoController een class met een naam als TodoRepositoryEnhancerBySpringCGLIB67548eb4 geïnjecteerd is; dit is een door Spring gegenereerde AOP proxy om onze TodoRepository heen.

We zijn even bezig geweest dit allemaal op te zetten maar dit schaalt fantastisch met je codebase. Deze code is her te gebruiken in elke component in elke (micro)service. En in plaats van een 4-tal regels extra code hoef je slechts de marker annotation toe te voegen per methode.

Als je simpelweg alle public methodes in al je repositories op deze manier wil verrijken heeft Spring AOP zelfs een Spring-specifieke Pointcut expressie: bean. Deze selecteert Spring beans bijvoorbeeld op basis van naam. Om alle repositories te selecteren kun je de expressie bean(*Repository) gebruiken.

In de voorbeeld applicatie op Github zijn nog een tweetal voorbeelden te vinden!

 

Conclusie

Ik ben al jaren een groot fan van Spring. Misschien wel het grootste pluspunt van Spring is hoe uitbreidbaar het is. In vrijwel iedere laag is code in te pluggen die bovendien simpel tussen applicaties te delen is. Naast servlet filters, interceptors, controller advice is er dus ook Spring AOP; waarschijnlijk wel de meest krachtige en flexibele manier om cross-cutting concerns af te handelen. Het kan toegepast worden op vrijwel elke methode in vrijwel elke Spring bean. En als Spring AOP niet krachtig genoeg is, is migreren naar AspectJ relatief simpel omdat Spring AOP volledig op AspectJ gebaseerd is.

Een belangrijke valkuil om in het achterhoofd te houden is dat het simpel is om ‘magie’ te introduceren in je (microservice) architectuur. Zorg dus dat iedereen in je team op de hoogte is als je AOP toepast en maak het indien mogelijk expliciet met marker annotations.

 

Referenties

  • Spring AOP documentatie: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
  • Voorbeeldapplicatie: https://github.com/nielsutrecht/spring-boot-aop
  • Spring voorbeeld applicatie: https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-aop
  • AspectJ programming guide: https://www.eclipse.org/aspectj/doc/released/progguide/index.html