Аспектно-ориентированное программирование (AOP) или почему мы отказались от использования самого маститого вендора AOP – PostSharp. Часть 2
Описание PostSharp
В предыдущей статье мы рассмотрели аспектно-ориентированное программирование и его преимущества, и то, чем мы руководствовались при выборе конкретного фреймворка для внедрения AOP в наши проекты. Как было сказано в итогах этого краткого введения, наш выбор остановился на PostSharp.
В данной части статьи мы подробнее расскажем о том, какие именно возможности предоставляет PostSharp и детально рассмотрим некоторые особенности реализации, использованные нами, которые позволяют считать PostSharp наиболее привлекательным AOP-фреймворком для .NET. А также раскроем наши причины отказа от PostSharp после нескольких лет успешного использования.
Итак, PostSharp – это аспектно-ориентированный фреймворк для платформы .NET.
Принцип работы PostSharp заключается в модификации IL-кода сборок .NET для добавления в них функционала, реализованного в рамках аспектов. Это достигается с помощью отдельного MSBuild-target-а, который выполняется после компиляции сборки. Картинка ниже иллюстрирует, какое место занимает PostSharp в процесса билда проектов.
Рисунок 1. Процедура билда с использованием PostSharp
Проекту PostSharp уже более 10 лет. За это время он прошел путь от персонального проекта с открытым исходным кодом до полноценного коммерческого продукта. Кратко рассмотрим эволюцию PostSharp.
Немного истории
Разработка PostSharp началась в 2004 году одним человеком по имени Гаэль Фрэто [Gael Fraiteur], живущем в Чехии. На тот момент самым популярным воплощением идей AOP был AspectJ, фреймворк для Java. В то время как для .NET, по мнению Гаэля, все решения были сырыми и малопригодными для практического использования. Таким образом, он взялся за разработку PostSharp в одиночку и в свободное от основной работы время.
В 2007 году, после череды пререлизов, была выпущена первая полноценная версия PostSharp. Она была полностью бесплатна, и исходный код был открыт под лицензией GPL3. Но после появления коммерческого интереса к проекту, Гаэль решил сосредоточить все своё время на PostSharp и задумался о монетизации своей разработки.
В 2010 году была выпущена вторая версия PostSharp со множеством новых возможностей и переписанным движком. Эта версия являлась уже полностью коммерческой. Гаэлем была основана компания PostSharp Technologies, которая сосредоточилась на разработке и поддержке PostSharp.
На момент написания статьи актуальной версией PostSharp является версия 5.0. В каждую новую версию, начиная со второй, включались всё новые возможности, эволюционировала модель монетизации, но описание всех этих изменений находится за рамками данной статьи. Можно отметить, что существует бесплатная версия, включающая все возможности и не имеющая временных ограничений. Но использование этих возможностей ограничено 10-ю классами и поэтому это подходит только для небольших проектов [см ссылку 3 в конце статьи].
Важно отметить, что исходный код PostSharp, начиная с версии 2.0, не включается ни в одну из публично доступных опций лицензирования.
PostSharp в ITA Labs
Мы начали внедрять PostSharp в наши проекты с 2012-го года. Использовалась одна из ранних версий, актуальная на тот момент. Эта версия используется нами до сих пор: обновление версии PostSharp не выполняли по причине того, что существующий в этой версии функционал продолжал нас устраивать.
Поэтому представленная далее информация о PostSharp является актуальной только для той версии и скорее служит для подробного описания тех возможностей, что используются нами.
Некоторые возможности PostSharp
В этом разделе мы подробно рассмотрим все те возможности PostSharp, что мы применяем в наших проектах, а именно:
- Аспект OnMethodBoundaryAspect;
- Инициализация аспектов во время компиляции через метод CompileTimeInitialize;
- Аспект MethodInterceptionAspect.
Аспект OnMethodBoundaryAspect
Начнем с самого простого аспекта OnMethodBoundaryAspect, позволяющего вставить свою логику в начале и в конце тела метода.
Классическим примером использования такого аспекта является добавление трассировки выполнения метода, когда мы хотим получить в логах сообщение о начале и конце выполнения метода.
Рассмотрим простейшую реализацию подобного аспекта:
Как видно, в классеTraceAttributeмы переопределили два метода:
- OnEntry, который вызывается в самом начале выполнения метода;
- OnExit, который вызывается в самом конце выполнения метода, независимо от того, происходит ли там ошибка или нет.
Каждый аспект PostSharp представляет собой кастомный атрибут C#. В этом плане OnMethodBoundaryAspect не является исключением.
Для того, чтобы применить аспект к нашему коду, достаточно применить атрибут к методу или классу:
Схематично PostSharp преобразует код метода для аспектов на основе OnMethodBoundaryAspect таким образом (чуть ниже мы рассмотрим, что генерируется на самом деле):
Внутри метода PostSharp генерирует инструкции с вызовом методов аспекта в начале и конце тела метода, оборачивая тело метода в блок try...catch для возможности обработки исключений логикой аспекта.
В дополнение к приведенным в примере выше методам, PostSharp поддерживает методы OnSuccess и OnException, для обработки успешного и ошибочного выполнения метода, соответственно.
Наверняка остаются вопросы: откуда берется переменная attribute? Как информация о методе и его аргументы передаются в аспект? Попробуем ответить на эти вопросы, более детально разобрав код, который на самом деле генерирует PostSharp.
Итак, обработанный PostSharp класс, если произвести дизассемблирование, будет выглядеть приблизительно так:
На что стоит обратить в данном относительно объемном куске кода:
- PostSharp генерирует отдельный класс (__$Aspects), содержащий статический конструктор и статические поля для атрибутов и методов. В статическом конструкторе происходит инициализация этих полей. Так как конструктор статический, то создание и инициализация всех полей происходит только один раз.
- PostSharp генерирует два статических поля для каждого применяемого атрибута. Одно поле содержит ссылку на экземпляр атрибута (a1в примере), а другое -- метаинформацию о методе, к которому был применен атрибут (m1).
- Информация о методе получается c использованием механизма Reflection, а конкретно метода MethodBase.GetMethodFromHandle, куда передаются ссылки на метод и тип, где определен метод.
- Как создается экземпляр атрибута a1 в приведенном коде не демонстрируется. Особенность PostSharp в том, что он создает объекты атрибутов во время компиляции. Более подробно об этом будет написано при описании метода CompileTimeInitializeниже.
- Однократная инициализация атрибута выполняется через отдельный метод: RuntimeInitialize.
- При вставке своего кода PostSharp использует сгенерированные статические поля, для которых вызывает соответствующие методы (OnEntry, OnExitи т.д.), передавая контекст выполнения в параметре inArgs.
Пожалуй, самым важным замечанием является то, что генерируемые поля и код их инициализации являются статичным. Если бы поля инициализировались, скажем, при каждом выполнении метода, то влияние PostSharp на производительность могло бы быть достаточно заметным из-за использования в коде инициализации Reflection.
Рассмотрим другую важную для нас возможность PostSharp.
Метод CompileTimeInitialize
Приведенный выше пример реализации TraceAttribute можно было бы переписать следующим образом:
По сравнению с предыдущей реализацией, сообщения с именем метода теперь хранятся в отдельных полях класса аспекта: _entryMessage и _exitMessage. Эти поля теперь инициализируются в добавленной перегрузке метода CompileTimeInitialize, а не при каждом вызове методов OnEntry и OnExit.
Метод CompileTimeInitialize вызывается только один раз и, что самое главное, он вызывается во время обработки PostSharp сборки, т.е. на этапе билда.
Дело в том, что все экземпляры атрибутов PostSharp создает на этапе компиляции, позволяя их инициализировать через этот метод. Далее, PostSharp сериализует все объекты и сохраняет их сериализованное представление внутри ресурсов сборки.
В приведенном выше примере необходимости в использовании CompileTimeInitialize в принципе нет. Но если бы формирование полей с сообщениями требовало более дорогостоящих манипуляций (например, активного использования Reflection), то перенос инициализации из рантайма на этап сборки был бы оправданным.
Аспект MethodInterceptionAspect
Аспект, "перехватывающий" выполнение декорируемого метода. В отличие от атрибутов, сделанных на основе OnMethodBoundaryAspect, в которых тело декорируемого метода вызывается всегда и только один раз, при перехвате метода мы можем не только отказаться от выполнения тела вызванного метода при каких-то условиях, но и полностью обернуть его выполнение, чтобы, например, как-то особенно обрабатывать исключения. Или даже вызвать его несколько раз.
Тем самым, атрибуты на основе MethodInterceptionAspect позволяют реализовать намного более нетривиальную логику.
В наших проектах этот атрибут решает несколько задач:
- Упрощение UI-кода, работающего в многопоточном режиме.
- Аутентификация пользователей, включающая в себя и нетривиальную логику, вроде проверки действительности и ограничений лицензии того или иного продукта.
Остановимся на более простой задаче по UI. В десктопной части наших проектов используется технология WinForms, и одной из особенностей данной технологии является обязательное требование выполнять обращения к GUI-контролам в отдельном GUI-потоке. И такое требование может принести много головной боли при реализации достаточно сложного UI с многопоточностью.
Для облегчения этой задачи нами был реализован отдельный атрибут, который содержит в себе весь необходимый boilerplate-код, необходимый для гарантии того, что тот или иной метод выполнится именно в UI-потоке, а не где-то ещё.
Итак, рассмотрим реализацию такого атрибута:
Пример использования:
В данном примере свойство args.Proceed является делегатом метода, содержащего логику декорируемого метода.
Нужное поведение, а именно вызов метода в GUI-потоке, достигается за счет генерации подобного кода:
В примере опущен код, генерируемый для инициализации полейaspectиmethod: он аналогичен коду, генерируемому для OnMethodBoundaryAspect.
Как видно из примера, PostSharp копирует тело декорируемого метода в новый сгенерированный метод и полностью заменяет код исходного метода. В декорируемый метод добавляется вызов метода атрибута, в который передается делегат, указывающий на сгенерированный метод.
Почему не PostSharp?
Выше мы описали все те возможности PostSharp, которые использовались в наших проектах для реализации следующего функционала:
- Сквозная трассировка через атрибут, подобный атрибуту TraceAttribute из этой статьи;
- Вызов методов GUI-контролов в GUI-потоке через атрибут OnGUIThreadAttribute;
- Аудит и аутентификация, с использованием атрибутов на основе базового атрибута OnMethodBoundaryAspect.
Из разбора кода, генерируемого PostSharp, можно сделать, что он ориентирован на производительность, достаточно качественен и хорошо продуман.
Вроде бы все хорошо, но почему же мы решили перейти с PostSharp на что-то другое?
Среди наших заказчиков есть и российские, продукты которых должны проходить сертификацию на отсутствие в них недокументированных возможностей по второму уровню (НДВ-2) в Федеральной службе по техническому и экспортному контролю (ФСТЭК). Это означает, что некоторые наши проекты должны проверяться на отсутствие не декларированных возможностей. Для сертификации по НДВ-2 в продукт нельзя включать компоненты без исходных кодов. При этом должна иметься возможность собрать проект на основе предоставленных исходников [см ссылку 2 в конце статьи].
Здесь и возникает проблема с PostSharp. Несмотря на то, что PostSharp по сути не является компонентом, при сборке он необходим. При этом PostSharp не сертифицирован, поэтому включать в поставку проекта PostSharp без исходных кодов нельзя. Начиная с версии 2.0, PostSharp является продуктом с закрытым исходным кодом и платной лицензией, которая не включает в себя исходный код продукта.
Поэтому, в итоге, мы решили отказаться от использования PostSharp в наших проектах в пользу Open Source-решения, которое для этого пришлось прилично переделать. О деталях миграции на новый AOP-фреймворк, сложностях и проблемах на этом пути, мы напишем в следующей части.
Автор статьи: Марат Вильданов, разработчик, ITA Labs
Ссылки
- Аспектно-ориентированное программирование (AOP) или почему мы отказались от использования самого маститого вендора AOP – PostSharp. Часть 1
- 5th Anniversary of PostSharp - History and Prospects. PostSharp Blog. 1 сентября 2009.
- Руководящий документ. Приказ председателя Гостехкомиссии России от 4 июня 1999 г. N 114.
- PostSharp Essentials: A free edition of PostSharp.