«1. Обзор

В этом кратком руководстве мы рассмотрим библиотеку Java jcabi-aspects, набор удобных аннотаций, которые изменяют поведение приложения Java с помощью аспектно-ориентированного программирования (АОП).

Библиотека jcabi-aspects предоставляет такие аннотации, как @Async, @Loggable и @RetryOnFailure, полезные для эффективного выполнения определенных операций с использованием АОП. В то же время они помогают уменьшить объем шаблонного кода в нашем приложении. Библиотека требует, чтобы AspectJ объединял аспекты в скомпилированные классы.

2. Настройка

Сначала мы добавим последнюю зависимость jcabi-aspects Maven в файл pom.xml:

<dependency>
    <groupId>com.jcabi</groupId>
    <artifactId>jcabi-aspects</artifactId>
    <version>0.22.6</version>
</dependency>

Для работы библиотеки jcabi-aspects требуется поддержка времени выполнения AspectJ. Поэтому давайте добавим зависимость AspectJRT от Maven:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.2</version>
    <scope>runtime</scope>
</dependency>

Далее добавим подключаемый модуль jcabi-maven-plugin, который объединяет двоичные файлы с аспектами AspectJ во время компиляции. Плагин предоставляет цель ajc, которая выполняет автоматическое сплетение:

<plugin>
    <groupId>com.jcabi</groupId>
    <artifactId>jcabi-maven-plugin</artifactId>
    <version>0.14.1</version>
    <executions>
        <execution>
            <goals>
                <goal>ajc</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
    </dependencies>
</plugin>

Наконец, давайте скомпилируем классы с помощью команды Maven:

mvn clean package

Журналы, сгенерированные jcabi-maven-plugin при компиляции, будут выглядеть так: :

[INFO] --- jcabi-maven-plugin:0.14.1:ajc (default) @ jcabi ---
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-loggable for watching of 
  @Loggable annotated methods
[INFO] Unwoven classes will be copied to /jcabi/target/unwoven
[INFO] Created temp dir /jcabi/target/jcabi-ajc
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-cacheable for automated
  cleaning of expired @Cacheable values
[INFO] ajc result: 11 file(s) processed, 0 pointcut(s) woven, 0 error(s), 0 warning(s)

Теперь, когда мы знаем, как добавить библиотеку в наш проект, давайте посмотрим на ее аннотации в действии.

3. @Async

Аннотация @Async позволяет выполнять метод асинхронно. Однако он совместим только с методами, которые возвращают тип void или Future.

Давайте напишем метод displayFactorial, который асинхронно отображает факториал числа:

@Async
public static void displayFactorial(int number) {
    long result = factorial(number);
    System.out.println(result);
}

Затем мы перекомпилируем класс, чтобы позволить Maven создать аспект для аннотации @Async. Наконец, мы можем запустить наш пример:

[main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-async for Asynchronous method execution

Как видно из журнала, библиотека создает отдельный поток демона jcabi-async для выполнения всех асинхронных операций.

Теперь давайте воспользуемся аннотацией @Async для возврата экземпляра Future:

@Async
public static Future<Long> getFactorial(int number) {
    Future<Long> factorialFuture = CompletableFuture.supplyAsync(() -> factorial(number));
    return factorialFuture;
}

Если мы используем @Async для метода, который не возвращает void или Future, во время выполнения будет выброшено исключение, когда мы его вызовем .

4. @Cacheable

Аннотация @Cacheable позволяет кэшировать результаты метода, чтобы избежать дублирования вычислений.

Например, давайте напишем метод cacheExchangeRates, который возвращает последние обменные курсы:

@Cacheable(lifetime = 2, unit = TimeUnit.SECONDS)
public static String cacheExchangeRates() {
    String result = null;
    try {
        URL exchangeRateUrl = new URL("https://api.exchangeratesapi.io/latest");
        URLConnection con = exchangeRateUrl.openConnection();
        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        result = in.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

Здесь кэшированный результат будет иметь время жизни 2 секунды. Точно так же мы можем навсегда сделать результат кэшируемым, используя:

@Cacheable(forever = true)

После того, как мы перекомпилируем класс и выполним его снова, библиотека зарегистрирует сведения о двух потоках демона, которые обрабатывают механизм кэширования:

[main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-cacheable-clean for automated 
  cleaning of expired @Cacheable values
[main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-cacheable-update for async 
  update of expired @Cacheable values

Когда мы вызываем наш метод cacheExchangeRates, библиотека кэширует результат и регистрирует детали выполнения:

[main] INFO com.baeldung.jcabi.JcabiAspectJ - #cacheExchangeRates(): 
'{"rates":{"CAD":1.458,"HKD":8.5039,"ISK":137.9,"P..364..:4.5425},"base":"EUR","date":"2020-02-10"}'
  cached in 560ms, valid for 2s

Таким образом, при повторном вызове (в течение 2 секунд) cacheExchangeRates вернет результат из кэша:

[main] INFO com.baeldung.jcabi.JcabiAspectJ - #cacheExchangeRates(): 
'{"rates":{"CAD":1.458,"HKD":8.5039,"ISK":137.9,"P..364..:4.5425},"base":"EUR","date":"2020-02-10"}'
  from cache (hit #1, 563ms old)

Если метод выдает исключение, результат не будет закэширован.

5. @Loggable

Библиотека предоставляет аннотацию @Loggable для простого ведения журнала с помощью средства ведения журнала SLF4J.

Давайте добавим аннотацию @Loggable к нашим методам displayFactorial и cacheExchangeRates:

@Loggable
@Async
public static void displayFactorial(int number) {
    ...
}

@Loggable
@Cacheable(lifetime = 2, unit = TimeUnit.SECONDS)
public static String cacheExchangeRates() {
    ...
}

Затем, после перекомпиляции, аннотация будет логировать имя метода, возвращаемое значение и время выполнения:

[main] INFO com.baeldung.jcabi.JcabiAspectJ - #displayFactorial(): in 1.16ms
[main] INFO com.baeldung.jcabi.JcabiAspectJ - #cacheExchangeRates(): 
'{"rates":{"CAD":1.458,"HKD":8.5039,"ISK":137.9,"P..364..:4.5425},"base":"EUR","date":"2020-02-10"}'
  in 556.92ms

6. @LogExceptions

Подобно @Loggable, мы можем использовать аннотацию @LogExceptions для регистрации только тех исключений, которые выдает метод.

Давайте используем @LogExceptions в методе DivideByZero, который вызовет исключение ArithmeticException:

@LogExceptions
public static void divideByZero() {
    int x = 1/0;
}

Выполнение метода запишет исключение в журнал, а также вызовет исключение:

[main] WARN com.baeldung.jcabi.JcabiAspectJ - java.lang.ArithmeticException: / by zero
    at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)

java.lang.ArithmeticException: / by zero
    at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)
    ...

7. @Quietly ~~ ~ Аннотация @Quietly похожа на @LogExceptions, за исключением того, что она не распространяет никаких исключений, создаваемых методом. Вместо этого он просто регистрирует их.

Давайте добавим аннотацию @Quietly к нашему методу DivideByZero:

Таким образом, аннотация проглотит исключение и зарегистрирует только детали исключения, которые в противном случае были бы сгенерированы:

@Quietly
public static void divideByZero() {
    int x = 1/0;
}

Аннотация @Quietly совместима только с методами, возвращающими тип void.

[main] WARN com.baeldung.jcabi.JcabiAspectJ - java.lang.ArithmeticException: / by zero
    at com.baeldung.jcabi.JcabiAspectJ.divideByZero_aroundBody12(JcabiAspectJ.java:77)

8. @RetryOnFailure

«Аннотация @RetryOnFailure позволяет нам повторить выполнение метода в случае исключения или сбоя.

Например, давайте добавим аннотацию @RetryOnFailure в наш методdivideByZero:

Итак, если метод выдает исключение, АОП-рекомендация попытается выполнить его дважды:

@RetryOnFailure(attempts = 2)
@Quietly
public static void divideByZero() {
    int x = 1/0;
}

Кроме того, мы можем определить другие параметры, такие как задержка, единица измерения и типы, при объявлении аннотации @RetryOnFailure: метод выдает исключение NumberFormatException.

[main] WARN com.baeldung.jcabi.JcabiAspectJ - 
#divideByZero(): attempt #1 of 2 failed in 147µs with java.lang.ArithmeticException: / by zero
[main] WARN com.baeldung.jcabi.JcabiAspectJ - 
#divideByZero(): attempt #2 of 2 failed in 110µs with java.lang.ArithmeticException: / by zero

9. @UnitedThrow

@RetryOnFailure(attempts = 3, delay = 5, unit = TimeUnit.SECONDS, 
  types = {java.lang.NumberFormatException.class})

Аннотация @UnitedThrow позволяет нам перехватывать все исключения, генерируемые методом, и заключать их в указанное нами исключение. Таким образом, он объединяет исключения, выдаваемые методом.

Например, давайте создадим метод processFile, который генерирует IOException и InterruptedException:

Здесь мы добавили аннотацию для переноса всех исключений в IllegalStateException. Таким образом, при вызове метода трассировка стека исключения будет выглядеть так:

10. Заключение

@UnitedThrow(IllegalStateException.class)
public static void processFile() throws IOException, InterruptedException {
    BufferedReader reader = new BufferedReader(new FileReader("baeldung.txt"));
    reader.readLine();
    // additional file processing
}

В этой статье мы рассмотрели библиотеку Java jcabi-aspects.

java.lang.IllegalStateException: java.io.FileNotFoundException: baeldung.txt (No such file or directory)
    at com.baeldung.jcabi.JcabiAspectJ.processFile(JcabiAspectJ.java:92)
    at com.baeldung.jcabi.JcabiAspectJ.main(JcabiAspectJ.java:39)
Caused by: java.io.FileNotFoundException: baeldung.txt (No such file or directory)
    at java.io.FileInputStream.open0(Native Method)
    ...

Во-первых, мы увидели быстрый способ настроить библиотеку в нашем проекте Maven с помощью jcabi-maven-plugin.

Затем мы рассмотрели несколько удобных аннотаций, таких как @Async, @Loggable и @RetryOnFailure, которые изменяют поведение приложения Java с помощью АОП.

Как обычно, все реализации кода доступны на GitHub.

«

As usual, all the code implementations are available over on GitHub.