Статьи

Ведение журнала методов Java с использованием AOP и аннотаций

Иногда я хочу регистрировать (через slf4j и log4j ) каждое выполнение метода, видеть, какие аргументы он получает, что он возвращает и сколько времени занимает каждое выполнение. Вот как я делаю это с помощью AspectJ , jcabi-aspect и Java 6 аннотации:

1
2
3
4
5
6
public class Foo {
  @Loggable
  public int power(int x, int p) {
    return Math.pow(x, p);
  }
}

Вот что я вижу в выводе log4j:

1
2
[INFO] com.example.Foo #power(2, 10): 1024 in 12μs
[INFO] com.example.Foo #power(3, 3): 27 in 4μs

Хорошо, не правда ли? Теперь посмотрим, как это работает.

Аннотация с сохранением во время выполнения

Аннотации — это техника, представленная в Java 6. Это инструмент метапрограммирования, который не меняет способ работы кода, но ставит отметки для определенных элементов (методов, классов или переменных). Другими словами, аннотации — это просто маркеры, прикрепленные к коду, которые можно увидеть и прочитать. Некоторые аннотации предназначены для просмотра только во время компиляции — они не существуют в файлах .class после компиляции. Другие остаются видимыми после компиляции и могут быть доступны во время выполнения.

Например, @Override относится к первому типу (его тип хранения — SOURCE ), а @Test из JUnit — ко второму типу (тип хранения — RUNTIME ). @Loggable — тот, который я использую в приведенном выше сценарии — это аннотация второго типа, из аспектов jcabi . После компиляции он остается с байт-кодом в файле .class .

Опять же, важно понимать, что хотя метод power() аннотирован и скомпилирован, он пока ничего не отправляет в slf4j. Он просто содержит маркер с надписью «пожалуйста, запишите мою казнь».

Аспектно-ориентированное программирование (АОП)

AOP — это полезный метод, который позволяет добавлять исполняемые блоки в исходный код без явного его изменения. В нашем примере мы не хотим регистрировать выполнение метода внутри класса. Вместо этого мы хотим, чтобы какой-то другой класс перехватывал каждый вызов метода power() , измерял время его выполнения и отправлял эту информацию в slf4j.

Мы хотим, чтобы этот перехватчик понимал нашу аннотацию @Loggable и регистрировал каждый вызов этого конкретного метода power() . И, конечно же, тот же перехватчик следует использовать для других методов, где мы разместим такую ​​же аннотацию в будущем.

Этот случай идеально соответствует первоначальному замыслу AOP — чтобы избежать повторения некоторых общих действий в нескольких классах.

Ведение журнала является дополнительной функцией к нашей основной функции, и мы не хотим загрязнять наш код несколькими инструкциями по ведению журнала. Вместо этого мы хотим, чтобы регистрация происходила негласно.

С точки зрения AOP наше решение можно объяснить как создание аспекта, который пересекает код в определенных точках соединения и применяет рекомендации, которые реализуют желаемую функциональность.

AspectJ

Давайте посмотрим, что означают эти волшебные слова. Но, во-первых, давайте посмотрим, как jcabi-аспекты реализуют их с помощью AspectJ (это упрощенный пример, полный код которого вы можете найти в MethodLogger.java ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Aspect
public class MethodLogger {
  @Around("execution(* *(..)) && @annotation(Loggable)")
  public Object around(ProceedingJoinPoint point) {
    long start = System.currentTimeMillis();
    Object result = point.proceed();
    Logger.info(
      "#%s(%s): %s in %[msec]s",
      MethodSignature.class.cast(point.getSignature()).getMethod().getName(),
      point.getArgs(),
      result,
      System.currentTimeMillis() - start
    );
    return result;
  }
}

Это аспект с одним советом around() внутри. Аспект @Aspect а совет — @Around . Как обсуждалось выше, эти аннотации являются просто маркерами в файлах .class . Они ничего не делают, кроме как предоставляют метаинформацию тем, кто интересуется средой выполнения.

Аннотация @Around имеет один параметр, который — в данном случае — говорит, что рекомендация должна применяться к методу, если:

  1. его модификатор видимости * ( public , protected или private );
  2. его имя — имя * (любое имя);
  3. его аргументы .. (любые аргументы); и
  4. это аннотировано @Loggable

Когда вызов аннотированного метода должен быть перехвачен, метод around() выполняется перед выполнением фактического метода. Когда вызов метода power() должен быть перехвачен, метод around() получает экземпляр класса ProceedingJoinPoint и должен возвращать объект, который будет использоваться в результате метода power() .

Чтобы вызвать оригинальный метод power() , совет должен вызвать proceed() объекта точки соединения .

Мы компилируем этот аспект и делаем его доступным в classpath вместе с нашим основным файлом Foo.class . Пока все хорошо, но нам нужно сделать последний шаг, чтобы претворить в жизнь наш аспект — мы должны применить наш совет.

Бинарный аспект плетения

Аспект плетения — это название процесса применения рекомендаций. Аспект ткач изменяет оригинальный код, вводя вызовы аспектов. AspectJ делает именно это. Мы даем ему два двоичных Java-класса Foo.class и MethodLogger.class ; он возвращает три модифицированных Foo.class , Foo$AjcClosure1.class и неизмененный MethodLogger.class .

Чтобы понять, какие советы и какие методы следует применять, AspectJ ткач использует аннотации из файлов .class . Кроме того, он использует отражение для просмотра всех классов на пути к классам. Он анализирует, какие методы удовлетворяют условиям из аннотации @Around . Конечно, он находит наш метод power() .

Итак, есть два шага. Сначала мы компилируем наши файлы .java с использованием javac и получаем два файла. Затем AspectJ переплетает / модифицирует их и создает свой собственный дополнительный класс. Наш класс Foo выглядит примерно так после плетения:

01
02
03
04
05
06
07
08
09
10
public class Foo {
  private final MethodLogger logger;
  @Loggable
  public int power(int x, int p) {
    return this.logger.around(point);
  }
  private int power_aroundBody(int x, int p) {
    return Math.pow(x, p);
  }
}

Ткач AspectJ перемещает нашу первоначальную функциональность в новый метод power_aroundBody() и перенаправляет все вызовы power() в аспектный класс MethodLogger .

Вместо одного метода power() в классе Foo теперь у нас есть четыре класса, работающих вместе. Отныне это происходит за кулисами при каждом обращении к power() :

неназванный

Оригинальная функциональность метода power() обозначена маленькой зеленой линией жизни на диаграмме.

Как видите, процесс создания аспектов связывает воедино классы и аспекты, передавая вызовы между ними через точки соединения. Без переплетения и классы, и аспекты являются просто скомпилированными двоичными файлами Java с прикрепленными аннотациями.

jcabi-аспекты

jcabi-aspect — это библиотека JAR, которая содержит аннотацию MethodLogger аспект MethodLogger (кстати, есть еще много аспектов и аннотаций). Вам не нужно писать свой собственный аспект для регистрации методов. Просто добавьте несколько зависимостей в ваш classpath и настройте jcabi-maven-plugin для создания аспектов (получите их последние версии в Maven Central ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<project>
  <depenencies>
    <dependency>
      <dependency>
        <groupId>com.jcabi</groupId>
        <artifactId>jcabi-aspects</artifactId>
      </dependency>
      <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
      </dependency>
    </dependency>
  </depenencies>
  <build>
    <plugins>
      <plugin>
        <groupId>com.jcabi</groupId>
        <artifactId>jcabi-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>ajc</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Поскольку эта процедура плетения требует больших усилий по настройке, я создал удобный плагин Maven с целью ajc , который выполняет всю работу по плетению аспектов. Вы можете использовать AspectJ напрямую, но я рекомендую использовать jcabi-maven-plugin .

Вот и все. Теперь вы можете использовать аннотацию @com.jcabi.aspects.Loggable и ваши методы будут регистрироваться через slf4j.

Если что-то не работает, как объяснено, не стесняйтесь представить вопрос Github .

Похожие сообщения

Вы также можете найти эти сообщения интересными:

Ссылка: Регистрация методов Java с помощью AOP и аннотаций от нашего партнера по JCG Егора Бугаенко в блоге About Programming .