a dev's blog

Some thoughts about thoughts.

Performance Messungen mit Spring AOP

2016-08-01 Development Java Spring

Intro

Tjaja… die Performance. Wie oft kommt eine Situation vor, dass man gerne wissen möchte welche Methodenausführung wie lange dauert. Es gibt da natürlich tolle Hilfsmittel wie XRebel von zeroturnaround, jedoch habe ich mich oft für einfachere Möglichkeiten interessiert.

Eine - wie ich finde - sehr schicke und schlichte Art der Performance Messung lässt mit mittels AOP realisieren. Dabei sieht das Ziel genau so aus, dass wir eine beliebige Methode mit einer Annotation versehen können und daraufhin im Log Performance-Messungen zu dieser Methode lesen können.

Setup

Wie immer habe ich ein Github-Projekt angelegt, was du dir anschauen kannst wenn dir die Ausführung hier nicht reichen sollte oder du lieber direkt aus Code lernst.

Zunächst einmal ist das Beispiel-Projekt mal wieder ein Spring-Boot-Projekt. Damit die Applikation nicht gleich terminiert und man von außen ein paar Aktionen ausführen kann, habe ich noch den Web-Starter hinzugefügt, so dass wir - zusammen mit AOP - am Ende zu der folgenden pom.xml kommen.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.3.2.RELEASE</version>
  <relativePath/>
</parent>

<!-- ... -->

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
</dependencies>
   

Annotation zum Markieren von zu loggenden Entries

Als nächstes legen wir eine Annotation an, um Methoden zu annotieren, für die wir am Ende im Log genaue Laufzeitinformationen erhalten wollen. Eine solche Annotation kann wie folgt aussehen:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Measure {

  /**
   * The name of the action.
   * @return
   */
  String action();
}

Das Markieren von zu loggenden Laufzeitinformationen

Das soeben angelegte Interface können wir jetzt benutzen um unsere Methoden zu annotieren. Hier zum Beispiel eine Methode aus einem BeerService (wie gesagt, zu finden in meinem Github-Projekt).

@Measure(action = "drinking")
public void drink() throws InterruptedException {
  LOG.info("skål");
  Thread.sleep(new Random().longs(1, 2).findFirst().getAsLong());
}

In der Methode wird zunächst ein skål im Log ausgegeben und danach 1-2 Sekunden lang gewartet. Die interessante Stelle ist allerdings die Annotation der Methode. Wir sagen hier, dass Messungen aufgezeichnet werden sollen mit der Action drinking. Diese Action soll später wieder im Log auftauchen.

Die Logging-Komponente

Spring bietet uns die Möglichkeit nicht nur die klassischen Spring-Annotations zu nutzen um eine Bean zu erzeugen oder an anderer Stelle zu injizieren oder in den Lifecycle aufzunehmen - nein. Vielmehr gibt es uns mit dem Paket spring-boot-starter-aop auch Möglichkeiten AOP-Funktionalität mit einfachen Annotations in unser Projekt zu ziehen. Zunächst einmal kannst du dir einen Überblick verschaffen, bevor ich auf die Details eingehe.

@Component
@Aspect
public class LogMeasurementFilter {

  /**
   * Aspect4j method which will be executed around other methods which are annotated with {@link Measure}.
   *
   * @param joinPoint the join point with the method execution.
   * @return the result of the method execution.
   * @throws Throwable
   */
  @Around("@annotation(Measure)")
  public Object logMeasurement(ProceedingJoinPoint joinPoint) throws Throwable {
    // determines the annotated action.
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    Measure annotation = method.getAnnotation(Measure.class);
    String measurementAction = annotation.action();

    // starts time measurement.
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    Logger logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
    logger.info(measurementAction + " started.");

    try {
      // execute the original method.
      return joinPoint.proceed();

    } finally {
      // stops execution measurement and log the data.
      stopWatch.stop();
      MDC.put("executionTime", stopWatch.getTotalTimeMillis());
      logger.info(measurementAction + " completed.");
      MDC.remove("executionTime");
    }
  }
}

Zunächst einmal erzeugen wir eine ganz normale Spring-Component. Dies passiert in Zeile 1. Daraufhin sagen mir mittels der @Aspect-Annotation, dass diese Komponente AOP-Funktionalität bereitstellt. Ab diesem Zeitpunkt können wir Aspekte wie Pointcut, Around, AfterThrowing, … nutzen.

In Zeile 12 und 13 nutzen wir einen solchen Aspekt, in dem wir eine Methode formulieren, die als einziges Argument einen JoinPoint erhält und die mittels @Around annotiert ist. In der Annotation setzen wir das Ziel, nämlich dass diese Methode für alle mit Measure annotierten Methoden ausgeführt werden soll. Und zwar wird die Methode logMeasurement aufgerufen, bevor die mittels Measure annotierte Methode aufgerufen wurde. Den Aufruf der Zielmethode müssen wir händisch in Zeile 28 durchführen.

Die anderen Zeilen sind relativ selbsterklärend. In den Zeilen 15-18 wird der Wert der Annotation ausgelesen. In den Zeilen 21-24 wird für die Zielklasse ein Logger instanziiert und für mehr Transparenz geloggt, dass jetzt die Ausführung einer annotierten Methode beginnt. In den Zeilen 32-35 wird die Laufzeit für die Action geloggt.

Fazit

Wird jetzt die Methode drink() aufgerufen taucht folgendes im Log auf (Vorrausgesetzt der MDC wird dort ausgegeben):

de.chclaus.examples.BeerService          : drinking started. |
de.chclaus.examples.BeerService          : skål |
de.chclaus.examples.BeerService          : drinking completed. | executionTime=2

Damit haben wir ganz schick eine Annotation eingeführt, mit der wir gezielte Methodenmessungen durchfüren können ohne einen großen Haufen Aufwand zu betreiben.

Cheers!