Tuesday, March 1, 2011

Знакомство с АОП

Парадигмы программирования


В современном мире IT-разработки существует довольно большое множество различных подходов к написанию программ. Так, например, кому-то нравиться представлять программу в виде последовательности действий, а кто-то считает, что программа должна представлять собой множество объектов, общающихся друг с другом. Совокупности этих идей и понятий образуют своего рода стиль написания программы, который принято назвать – парадигма программирования.

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

  • инструкция (императивное программирование, FORTRAN/C/PHP),

  • функция (функциональное программирование, Haskell/Lisp/F#/Scala),

  • прототип (прототипное программирование, JavaScript),

  • объект (объектно-ориентированное программирование, С++/Java),

  • факт (логическое программирование, PROLOG).


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

В этой статье я хочу рассказать о сравнительно молодой, но крайне, на мой взгляд, полезной парадигме программирования – аспектно-ориентированном программировании.




Основы АОП


Рассмотри некоторую сферическую службу в вакууме (например, web-сервис), реализующую следующий метод:
public BookDTO getBook(Integer bookId) {
BookDTO book = bookDAO.readBook(bookId);
return book;
}

Метод довольно прост и очевиден: чтение информации о некоторой книге по её идентификатору. Но давайте подумаем, чего тут не хватает? Первым делом нам стоит задуматься о логировании – без него, как вы сами понимаете, в web-службе никуда:
public BookDTO getBook(Integer bookId) {
LOG.debug("Call method getBook with id " + bookId);

BookDTO book = bookDAO.readBook(bookId);

LOG.debug("Book info is: " + book.toString());
return book;
}


Далее необходимо реализовать обработку исключений (сделать так, что бы слой служб возвращал соответствующие ему исключения, скрывая исключения нижележащих слоёв):

public BookDTO getBook(Integer bookId) throws ServiceException {
LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;

try {
book = bookDAO.readBook(bookId);
} catch(SQLException e) {
throw new ServiceException(e);
}


LOG.debug("Book info is: " + book.toString());
return book;
}


Так же не стоит забывать о проверке прав доступа:

public BookDTO getBook(Integer bookId) throws ServiceException, AuthException {
if (!SecurityContext.getUser().hasRight("GetBook"))
throw new AuthException("Permission Denied");


LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;

try {
book = bookDAO.readBook(bookId);
} catch(SQLException e) {
throw new ServiceException(e);
}

LOG.debug("Book info is: " + book.toString());
return book;
}


Кроме того имеет смысл кешировать результат работы:

public BookDTO getBook(Integer bookId) throws ServiceException, AuthException {
if (!SecurityContext.getUser().hasRight("GetBook"))
throw new AuthException("Permission Denied");

LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;
String cacheKey = "getBook:" + bookId;

try {
if (cache.contains(cacheKey)) {
book = (BookDTO) cache.get(cacheKey);
} else {

book = bookDAO.readBook(bookId);
cache.put(cacheKey, book);
}

} catch(SQLException e) {
throw new ServiceException(e);
}

LOG.debug("Book info is: " + book.toString());
return book;
}


Можно продолжать совершенствовать данный метод, но для начала - достаточно. В ходе наших доработок мы получили метод в 10 раз (с 2 до 20 LOC) превышающий исходный размер. Самое интересное, что объём бизнес-логики в нём не изменился – это всё та же 1 строка. Остальной код реализует некоторую общую служебную функциональность приложения: логирование, обработку ошибок, проверку прав доступа, кеширование и так далее.

В принципе, переплетение бизнес-логики со служебным функционалом не так страшно, пока ваше приложение невелико. Однако, чем сложнее становится программа, тем более тщательно следует подходить к её архитектуре в целом и выделении общей функциональности в частности. Именно поэтому, наблюдая за эволюцией языков программирования, сначала мы видим появление функций, потом модулей, затем объектов. Однако, практика показывает, что для выделения некоторой общей функциональности, упомянутых выше парадигм недостаточно. Такую функциональность называют «сквозной» или «разбросанной», в виду того, что её реализация действительно разбросана по разным частям приложения. Примерами сквозной функциональности, как мы уже видели выше, могут служить:

Основной задачей аспектно-ориентированного программирования (АОП) является модуляризация сквозной функциональности, выделение её в аспекты. Для этого языки, поддерживающие концепцию АОП, реализуют следующие средства для выделения сквозной функциональности:

  • аспект (aspect) – модуль или класс, реализующий сквозную функциональность. Аспект изменяет поведение остального кода, применяя совет в точках соединения, определённых некоторым срезом. Так же аспект может использоваться для внедрения функциональности;

  • совет (advice) – дополнительная логика - код, который должен быть вызван из точки соединения. Совет может быть выполнен до, после или вместо точки соединения;

  • точка соединения (join point) - точка в выполняемой программе (вызов метода, создание объекта, обращение к переменной), где следует применить совет ;

  • срез (pointcut) - набор точек соединения. Срез определяет, подходит ли данная точка соединения к заданному совету;

  • внедрение (introduction) - изменение структуры класса и/или изменение иерархии наследования для добавления функциональности аспекта в инородный код;

  • цель (target) – объект, к которому будут применяться советы;

  • переплетение (weaving) – связывание объектов с соответствующими аспектами (возможно на этапе компиляции, загрузки или выполнения программы).




Пример использования (AspectJ)


AspectJ является аспектно-ориентированным расширением/framework’ом для языка Java. На данный момент это, пожалуй, самый популярный и развивающийся АОП движок.

Рассмотрим реализацию аспекта логирования с его помощью:
@Aspect
public class WebServiceLogger {
private final static Logger LOG =
Logger.getLogger(WebServiceLogger.class);

@Pointcut("execution(* example.WebService.*(..))")
public void webServiceMethod() { }

@Pointcut("@annotation(example.Loggable)")
public void loggableMethod() { }

@Around("webServiceMethod() && loggableMethod()")
public Object logWebServiceCall(ProceedingJoinPoint thisJoinPoint) {
String methodName = thisJoinPoint.getSignature().getName();
Object[] methodArgs = thisJoinPoint.getArgs();

LOG.debug("Call method " + methodName + " with args " + methodArgs);

Object result = thisJoinPoint.proceed();

LOG.debug("Method " + methodName + " returns " + result);

return result;
}
}


Первым делом создаётся аспект логирования методов сервисов – класс WebServiceLogger, помеченный аннотацией @Aspect. Далее определяются два среза точек соединения: webServiceMethod (вызов метода, принадлежащего классу WebService) и loggableMethod (вызов метода, помеченного аннотацией @Loggable). В завершении объявляется совет (метод logWebServiceCall), который выполняется вместо (аннотация @Around) точек соединения, удовлетворяющих срезу ("webServiceMethod() && loggableMethod()").

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

AspectJ обладает довольно большим объёмом поддерживаемых срезов точек пересечения. Ниже приведены основные из них:

  • execution(static * com.xyz..*.*(..)) – выполнение кода любого статического метода в пакете com.xyz;

  • call(void MyInterface.*(..)) – вызов любого метода, возвращающего void, интерфейса MyInterface;

  • initialization(MyClass || MyOtherClass) – инициализация класса MyClass или MyOtherClass;

  • staticinitialization(MyClass+ && !MyClass) – статическая инициализация класса, имя которого начинается на MyClass, но не сам MyClass;

  • handler(ArrayOutOfBoundsException) – выполнение обработчика исключения ArrayOutOfBoundsException;

  • get/set(static int MyClass.x) - чтение / запись свойства x класса MyClass;

  • this/target(MyClass) – выполнение точки соединения, соответствующей объекту типа MyClass;

  • args(Integer) – выполнение точки соединения, в которой доступен аргумент типа Integer;

  • if(thisJoinPoint.getKind().equals("call")) – совпадает со всеми точками соединения, в которых заданное выражение истинно;

  • within/withincode(MyClass) - совпадает со всеми точками соединения, встречающимися в коде заданного класса;

  • cflow/cflowbelow(call(void MyClass.test())) – совпадает со всеми точками соединения, встречающимися в потоке выполнения заданного среза;

  • @annotation(MyAnnotation) – выполнение точки пересечения, цель которой помечена аннотацией @MyAnnotation.



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

  • before – запуск совета до выполнения точки соединения,

  • after returning - запуск совета после нормального выполнения точки соединения,

  • after throwing - запуск совета после выброса исключения в процессе выполнения точки соединения,

  • after - запуск совета после любого варианта выполнения точки соединения,

  • around – запуск совета вместо выполнения точки соединения (выполнение точки соединения может быть вызвано внутри совета).


Подробнее о конструкциях AspectJ можно прочитать в соответствующем разделе [1,2] официальной документации.

Для того, что бы использовать аспекты AspectJ их придётся скомпилировать и «вшить» в основные классы с помощью специального компилятора AJC.

Продукт бесплатный. Распространяется под Eclipse License.


Пример использования (PostSharp)



PostSharp является аспектно-ориентированным framework’ом для платформы .NET. Существуют и другие реализации АОП для .NET, однако, судя по сравнениям с сайта PostSharp, лидирующую позицию занимает именно он.

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

public class ExceptionDialogAttribute : OnExceptionAspect
{
public override void OnException(MethodExecutionEventArgs eventArgs)
{
string message = eventArgs.Exception.Message;
Window window = Window.GetWindow((DependencyObject)eventArgs.Instance);
MessageBox.Show(window, message, "Exception");
eventArgs.FlowBehavior = FlowBehavior.Continue;
}
}


Строго говоря, аспекты в терминологии PostSharp – это, как мы можем видеть, аспект и совет в терминологии АОП.

Для того, что бы указать срез точек пересечения для данного аспекта необходимо в файл настроек сборки (AssemblyInfo.cs) добавить следующую строку:

[assembly: ExceptionDialog ( AttributeTargetTypes="Example.WorkflowService.*",
AttributeTargetMemberAttributes = AttributeTargetElements.Public )]


Или же явно пометить интересующие вас методы атрибутом ExceptionDialog:

[ExceptionDialog]
public BookDTO GetBook(Integer bookId)


Вот собственно и всё: теперь все выброшенные в соответствующих методах исключения будут обрабатываться созданным аспектом.

В виду того, что PostSharp частично склеивает понятия совета и аспекта, последних у него получается довольно немало. Подробно с ними можно познакомиться в документации. Ниже приведены основные из них:

  • OnMethodBoundary/OnMethodInvocation – обращение к методу (начало, конец, выход, выход с исключением);

  • OnFieldAccess – обращение к свойству;

  • OnException – обработка исключения;

  • Composition – внедрение кода;



Для работы PostSharp’у необходим компилятор и библиотека, которые нужно подключить к проекту. Вшивание аспектов основано на post-обработке байт-кода во время сборки приложения.

Продукт платный. Есть Community Edition.


От теории к практике



И так, мы только что увидели, как красиво и эффективно можно решить проблему «выноса за скобки» сквозного функционала в вашем приложении. Однако, это всё теория. На практике всё, естественно, немного иначе :)

Прежде всего, в обоих случаях для компиляции и «вшивания» (weaving) аспектов придётся использовать специальный компилятор и тащить вместе с проектом дополнительные библиотеки. Вроде бы, это не проблема: компилятор легко скачивается и интегрируется в среду (например, при использовании maven’a задача сведётся всего лишь к добавлению плагина aspectj-maven-plugin), а множество зависимостей – обычное дело, по крайней мере для Java-приложений (решаемая с помощью того же maven’a) . Однако, необходимость включения в проект чего-то, что требует отдельной компиляции, да ещё и не имеет широкого распространения, зачастую отпугивает разработчиков, не смотря на все потенциальные плюсы.

В данном случае решением проблемы может стать Spring Framework [1,2]. Данный фреймворк имеет много достоинств, однако в рамках данной статьи нас интересует его AOP-составляющая. Spring Framework реализует ограниченную AOP-функциональность на чистом Java (C#) без использования сторонних библиотек с помощью создания прокси-объектов (JDK Dynamic Proxy, CGLIB). Другими словами в Spring AOP можно использовать только точки соединения типа «выполнение метода». Однако, как показывает практика, данное ограничение не играет значительной роли, так как для решения большинства задач, требуется точки соединения именно этого типа.

Кроме того, Spring Framework поддерживает конфигурирование приложений c помощью @AspectJ аннотаций, а так же интеграцию аспектов скомпилированных непосредственно с помощью AspectJ.

У себя в компании мы используем именно Spring AOP. Учитывая прочие заслуги Spring Framework, на мой взгляд, он является самой доступной и удобной площадкой для работы с AOP, внося значительный вклад в его популяризацию и развитие.


Резюме



Подводя итоги, хочется выделить три основные мысли относительно АОП:

  • Основная цель АОП - выноса «общей» (сквозной) функциональности «за скобки» (модуляризация сквозной функциональности);

  • Для Java AOP доступен через проект AspectJ, для .NET – через PostSharp;

  • Наиболее простая и проверенная реализация AOP – Spring AOP.




Ссылки по теме





P.S.


Во время в вёрстки этой статьи наткнулся на довольно няшную и в тоже время простую систему подсветки кода. На много приятней, чем Source Code Highlighter из Хабраредактора.

1 comment: