Separation of Concerns mit Spring AOP

23.11.2020

Mit Aspekt Orientierter Programmierung (AOP) können Querschnittsaufgaben wie Logging, Parameterprüfung, Authentifizierung / Autorisierung in einem eigenen Aspekt behandelt werden anstatt diese im Quellcode zu verstreuen. Auf diese Weise hilft AOP, das Separation of Concerns Prinzip umzusetzen.

Spring AOP

Spring AOP ist zum Vergleich von anderen AOP-Frameworks aus grundsätzlichen technologischen Gründen sehr abgespeckt. Für unseren Anwendungsfall genügte es jedoch und bot sich an, da wir uns ohnehin im Spring-Umfeld bewegen.

Manch andere AOP-Implementierungen wie z.B. AspectJ, fügen die übergreifenden Belange vor der Laufzeit direkt in den eigentlichen Code ein. Spring AOP basiert dagegen auf dem Proxy-Muster. Daher muss es von der betreffenden Java-Klasse eine Unterklasse bilden, um die übergreifenden Belange anwenden zu können. Der letztgenannte Ansatz schränkt die möglichen Joinpoints deutlich ein (mehr Details dazu und zu weiteren Vor- / Nachteilen finden sich auf baeldung.com).

Interceptor

Interceptors unterbrechen den Programmfluss und schieben sich in dem Ausführungspfad ein. An den Stellen können dann sogenannte Einschubmethoden ausgeführt werden. Je nachdem, wann Einschubmethoden ausgeführt werden sollen, werden die Methoden mit before, after oder around deklariert.

Joinpoint

Ein Joinpoint ist die Stelle, an denen man sich in ein bestehendes Programm einklinken kann. Joinpoints können Methodenausführungen, Zugriffe auf Attribute eines Objektes, Aufrufe einer Operation, die Erstellung von Objekten oder das Auslösen von Exceptions definieren. In Spring AOP beziehen sich Joinpoints jedoch nur auf die Ausführung von Methoden.

Pointcut

Ein Pointcut legt fest, welche Joinpoints als Einstiegspunkte genutzt werden. Joinpoints definieren nur mögliche Einstiegspunkte, Pointcuts hingegen legen dann fest, welche Joinpoints tatsächlich genutzt werden.

Restaurant-Analogie

Wenn man sich in ein Restaurant begibt, hat man eine Karte mit verschiedenen Gerichten. Die Gerichte auf der Karte sind nur „Gelegenheiten zum Essen“ (Joinpoints). Sobald man aber ein oder mehrere Gerichte bestellt hat (Pointcut), wartet man darauf, bis der Kellner es einem bringt. Die Joinpoints sind die Gerichte auf der Karte und der Pointcut ist das was man ausgewählt hat.

Advice

Ein Advice beschreibt, was an den definierten Joinpoints eines Pointcuts passiert. Advices sind Anweisungen, die festlegen, welche Interceptoren an den von Pointcuts selektierten Joinpoints in den Programmfluss eingefügt werden.

Aspect

Ein Aspect fasst mehrere Pointcuts und Advices zusammen.

Separation of Concerns konformes Parameterescaping mit Spring AOP

In unserem Projekt genügte Spring AOP vollkommen aus. Konkret ging es bei uns darum, das Escaping von Parametern für Solr-Queries durchzuführen. Bisher geschah dies in einem Wrapper. Dieser delegierte nach dem Escaping an die eigentliche Klasse. Dies ist jedoch wartungsintensiv. Zudem widerspricht dieser Ansatz dem Separation of Concerns Prinzip.
Spring AOP bietet die Möglichkeit, die Parameter einer aufgerufenen Methode zu verändern und dann an die ursprünglich aufgerufene Methode weiterzugeben. Wie dies funktioniert und getestet werden kann, zeige ich in den nachfolgenden Abschnitten.

Um das nachfolgende Beispiel einfacher zu halten verwende ich statt des Parameter-Escapings aus dem Projekt ein simples Whitespace-Trimming.

In einem reinen Spring-Projekt müsste man die Annotation @EnableAspectJAutoProxy, um AOP zu aktivieren. In Spring-Boot-Projekten wird dies jedoch bereits von der @SpringBootApplication-Annotation übernommen.

Der Aspect mit Joinpoints und Pointcuts

Wenn man Parameter einer Methode manipulieren möchte, benötigt man einen @Around-Advice. Dieser kann sowohl vor als auch nach der Ausführung der Methode eingreifen.

@Component
@Aspect
public class TrimmingAspect {

    @Around("execution(* com.demo.project.repository.SomeRepository.findSomething(..))")
    public Object trimParamValuesOfFindSomethingQueries(ProceedingJoinPoint pjp) throws Throwable {
        return pjp.proceed(trimParams(pjp.getArgs()));
    }

    private Object[] trimParams(Object[] args) {
        for (int index = 0; args.length > index; index++) {
            if (args[index] instanceof String) {
                args[index] = ((String) args[index]).trim();
            }
        }
        return args;
    }

}    

@Around

Mit der @Around-Annotation wird hier der Pointcut execution festgelegt. In den Klammern der execution wird die Signatur der Methode angegeben. Der Stern * fungiert hierbei als Wildcard. Dies ist dann hilfreich, wenn es z.B. mehrere Methoden gibt, die den gleichen Namen haben aber unterschiedliche Typen von Rückgabewerten liefern. So kann mit „* com.demo.project.MyClass.find*“ einen Pointcut auf alle Methoden der Klasse „de.firma.projekt.MyClass“ setzen, die mit „find“ beginnen und einen beliebigen Datentyp zurückliefern. In den Klammern der Execution lässt sich mit zwei Punkten eine beliebige Anzahl an Parametern definieren.  Mit Plus lässt sich angeben, dass mindestens ein Parameter vorhanden sein muss.

Über den ProceedingJoinPoint kann das Array mit Argumenten gelesen oder bearbeitet werden.

Mit pjp.proceed(..) wird die als Pointcut festgelegte Methode ausgeführt. Danach kann weiterer Code ausgeführt werden z.B. Log-Ausgaben.

@Before, @After

Mit diesen Annotationen, kann zusätzlicher Code vor und nach der Methode ausgeführt werden.

@AfterReturning

Mit AfterReturning ist es möglich den Rückgabewert zu erhalten und diesen z.B. für Logging-Zwecke zu verwenden.

@Component 
@Aspect 
public class LoggingAspect { 
         
    @AfterReturning(value = "execution(* com.demo.project.repository.MyClass.findSomething(..))", returning = "page") 
    private void logFindSomething(Page<Something> page) { 
        LOG.info("Found {} somethings", page.getTotalElements()); 
    }  
}

Poincut-Designators

Der hauptsächliche Pointcut-Designator bei Spring AOP ist „execution“:

@Pointcut("execution(public String com.demo.project.MyClass.myMethod(String)")

Dieser Pointcut wird dann getriggert, wenn die angegebene Methode ausgeführt wird. Wie weiter oben schon erläutert, können mit (*) und (..) die Pointcuts flexibler definiert werden.

Es gibt noch einige weitere Pointcut-Designators z.B. für Pointcuts, die sich auf Annotationen, Argumenttypen u.a. beziehen. Mehr hierzu findet sich in der offiziellen Dokumentation.

Pointcuts kombinieren

Pointcuts lassen sich auch mit  &&, || und ! kombinieren. Wichtig bei Annotationen mit @Pointcut ist, das der Methodenrumpf leer bleibt.

  @Pointcut("@target(com.demo.project.repository)")
  public void repositoryMethods(){}

  @Pointcut("execution(* *..find*(..))")
  public void allFindMethods(){}
  
  @Pointcut("repositoryMethods() && allFindMethods()")
  public void combinePointcuts(){}

  @Before("combinePointcuts()") 
  public void doSomethingWithPointcuts(){ 
    //.. 
    //Code 
    //.. 
  }

Wie testet man Spring Aspects?

Um die Aspects ohne kompletten Applikationskontext zu testen muss der Proxy in einer Setup-Methode initialisiert werden. So können dann auf proxy die Methoden der Zielklasse aufgerufen werden, damit die Pointcuts ausgelöst werden.

    ...

    @Mock
    private SomeRepository repositoryMock;

    private SomeRepository proxy;

    @BeforeEach
    void setUp() {
        AspectJProxyFactory factory = new AspectJProxyFactory(repositoryMock);
        TrimmingAspect repositoryAspect = new TrimmingAspect();
        factory.addAspect(repositoryAspect);
        proxy = factory.getProxy();
    }

    @Test
    void testTrimSpaces() {
        proxy.findSomething(" trimSpaces\t  ");
        // Überprüfen, ob die Methode im Repository tatsächlich mit getrimmten Parameterwerten aufgerufen wird
        Mockito.verify(repositoryMock).findSomething("trimSpaces");
    }

    ...

Quellen

Bildnachweis: Bild Honigbiene (Basis für Titelbild): pixabay.com

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*