27 January, 2012

Немножко магии от AspectJ

Наверно, вы уже сталкивались с таким понятием, как AOП - аспектно-ориентированное программирование.

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

Но это не единственные области применения АОП.

Я хочу показать ещё пару областей применения из реальных проектов:

1. Модификация исходного кода для реализации дополнительных возможностей.
2. Принудительная проверка контракта между модулями.

Модификация исходного кода для реализации дополнительных возможностей

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

service.doUsefulThing();

наш вызов превращается в

try {
        service.doUsefulThing();
    } catch ( FirstServiceException e) {
        processException(e);
    } catch ( SecondServiceException e) {
        processException(e);
    }

Дополнительная проблема в том, что у модуля количество модулей 10+, количество методов также велико, что приводит к тому, что блоки try/catch замусоривают код. Решение с использованием паттерна Callback также приведёт к замусориванию кода.

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

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

package com.blogger.atamanenko;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
@Inherited
public @interface SuppressExceptions {
}
    

А также аспект, который бы делал всю нужную нам функциональность:

public aspect ExceptionSupressingAspect {

        declare soft :ServiceException:  execution(@com.blogger.atamanenko.annotation.SuppressExceptions * *.*(..));

}

Данный аспект делает в точности следующее: "Смягчает" исключение ServiceException для метода, который помечен аннотацией @SuppressExceptions.

Пример использования:

@SuppressExceptions
    protected Entity findEntity(final Identifiable id) {
        return entityService.findById(id);
    }

Принудительная проверка контракта между модулями

Часто нам необходимо принудительно требовать выполнения каких-то архитектурных ограничений, например, контроллеры должны работать только с сервисами и им запрещено напрямую обращаться к базе данных.

Для реализации таких проверок можно также использовать возможности AOP.

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

aspect ForbidAccessToModelAspect {

//      Full prohibition of access to model:
        pointcut accessModel(): call(* com.blogger.atamanenko.app.model..*.*(..));
        declare error: accessModel() : "Illegal call to model";

}

После объявления такого аспекта, мы получим ошибку компиляции, что, очевидным образом, приведёт к выполнению архитектурного ограничения.

Если же нам необходимо разрешить доступ к одному пакету только из какого-то определённого другого, то мы можем модифицировать наш аспект следующим образом:

aspect ForbidAccessToModelAspect2 {

        pointcut accessModel(): call(* com.blogger.atamanenko.app.model.**.*(..));

        // Allow access to model from specific package for methods and constructors
        pointcut allowAccessModelFromSpecificPackage(): withincode(* com.blogger.atamanenko.app.allowedpackage..*.*(..));
        pointcut allowAccessModelFromSpecificPackage2(): withincode(com.blogger.atamanenko.app.allowedpackage..*.new(..));

        // forbid usage from any other methods.
        declare error: accessModel() && !(allowAccessModelFromSpecificPackage() || allowAccessModelFromSpecificPackage()):"Illegal call to Model from forbidden package";

}


Такой аспект, созданный в нашем модуле запретит нам использовать классы модели из всех пакетов, кроме com.blogger.atamanenko.app.allowedpackage

Сборка приложения

Файл аспекта нужно положить в каталог src/main/aspect, а для сборки приложения необходимо использовать не стандартный Oracle Java Compiler, а AspectJ compiler.

Пример конфигурации для Apache Maven:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>${aspectj-maven-plugin.version}</version>
    <configuration>
        <complianceLevel>1.6</complianceLevel>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
        <verbose>true</verbose>
    </configuration>
    <executions>
        <execution>
            <phase>process-sources</phase>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Заключение

Вот в общем-то и всё. Я сознательно не стал описывать языковые конструкции аспектов, так как они подробно описаны в руководстве AspectJ