Aop framework что это

Обновлено: 07.07.2024

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

Стратегия реализации AOP

(1) Java SE Dynamic Agent: используйте динамический агент для динамического генерации метода реализации объекта в период работы одного или нескольких интерфейсов, которые могут добавлять код расширения при реализации интерфейса в сгенерированном объекте, тем самым реализующий AOP Отказ Недостатком является то, что он может быть только прокси для интерфейса, и поскольку динамический агент реализован по отражению, иногда необходимо учитывать накладные расходы на вызов отражения. (2) Generation Bytecode (CGLIB Dynamic Agent) Динамическая технология генерирования инакопии в качестве подкладка относится к объекту подкласса, которое генерирует указанный класс во время выполнения, и перезаписывает определенный метод, а код расширения может быть добавлен, когда метод покрыт., Его общие инструменты являются CGLIB. (3) Настраиваемые классовые погрузчики должны быть добавлены ко всем объектам классов, Dynamic Proxy and Bytecode Generation требуют динамических конструктивных объектов агента, то есть в конечном итоге усовершенствованный объект генерируется AOP Framework, а не разработчика New. Решение состоит в том, чтобы реализовать пользовательский классный погрузчик и улучшить его, когда нагружен класс. Jboss - это реализовать функцию AOP таким образом. (4) Инструмент утилизации генерации кода генерирует новый код на основе существующего кода, который может добавить любой поперечный код для реализации AOP. (5) Расширение языка может быть улучшено для работы по назначению конструктора и атрибута, и Aspectj - это общее расширение языка Java, которое реализует AOP таким образом.

Терминология AOP

1) jointoint.

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

2) вырезать точка

Каждый класс программы имеет несколько точек подключения, такие как классы с двумя методами, оба из которых являются точками подключения, то есть точка подключения - это вопрос зрения в классе программ. AOP находит определенную точку соединения через «точку резания». Точка подключения эквивалентна записи в базе данных, и точка среза эквивалентна условию запроса. Точки толщины и точки подключения не являются отношениями на одном на одном, а точка среза может соответствовать нескольким точкам соединения. Весной точки очистки описаны через интерфейс org.springframework.aop.contcut, который использует классы и методы в качестве критериев запроса точки соединения, а анализатор правила пружины AO отвечает за набор наборов критерия запросов, найти соответствующее соединение. Point. На самом деле, его нельзя назвать точкой подключения запросов, поскольку точка подключения находится перед выполнением метода, выполнение определенной точки выполнения программы, включающей в себя информацию о ориентации, и точка обрезания расположена только в методе, так что если Желается, чтобы быть расположенным к конкретному в точке соединения, вам также необходимо обеспечить информацию о ориентации.

3) Улучшение (совет)

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

4) Целевой объект (цель)

Усиленная логика для сплетения целевого класса. Если нет AOP, целевой класс услуг должен реализовывать всю логику, и с помощью AOP класс целевого обслуживания реализует только логику нежировкой логики, а также управление мониторингом производительности, эти кросс-среза Логика может использовать динамику AOP. Вплеть в определенную точку соединения.

5) ВВЕДЕНИЕ

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

6) ткачество (плетение)

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

А, скомпилированное сплетенное, которое требует специального компилятора Java.

B, тип загрузки, который требует специального типа нагрузки.

C. Динамический агент сплетена, добавляя расширенный генерирующий подкласс в течение периода работы.

Весна использует динамические агенты для сплетены, и Aspectj использует период компиляции для сплетенного и загруженного.

7) агент (прокси)

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

8) Swipe (аспект)

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

Тематическое исследование

Прокси-модель - это обычный режим дизайна Java, разделенный на две модели прокси в соответствии с созданием режима агента.

Режим статического агента:

Автоматически генерировать исходный код программистами или конкретными инструментами, а также компилируйте их. Перед запуском программы файл класса Agent's Class's .Class файл уже существует.

Режим динамического агента:

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

Приложения

Статический агент

JDK динамический агент

Примечание: динамические агенты JDK основаны на реализации интерфейса

CGLIB динамический агент

Динамический прокси-механизм JDK может только прокси интерфейс классов и не может реализовать интерфейс интерфейса, динамический агент JDK не может быть реализован. CGLIB является прокси для классов. Его принцип состоит в том, чтобы генерировать подкласс для назначенного целевого класса. И переоценивать Метод для достижения улучшения, но поскольку наследование используется, он не может быть прокси для окончательного модифицированного класса.

JDK динамический агент и разница динамического агента CGLIB

Динамический агент Java - это рефлекторный механизм для генерации анонимного класса, который реализует интерфейс прокси и вызовов InvokeHandler перед вызовом конкретного метода.

Динамический агент CGLIB использует пакет с открытым исходным кодом ASM, загрузите файл класса класса объекта агента и обрабатывает его, изменяя его сгенерированный подкласс Bytecode.

1. Если целевой объект реализует интерфейс, динамический агент JDK будет реализовывать AOP 2 по умолчанию. Если целевой объект реализует интерфейс, вы можете принудительно использовать CGLIB для реализации AOP.

3. Если целевой объект не реализует интерфейс, необходимо использовать библиотеку CGLIB, а весна автоматически преобразует динамический прокси JDK и CGLIB.

JDK Dynamic Proxy и CGLIB Bytecode Разница генерации? (1) Динамический агент JDK может генерировать только прокси для классов, которые реализуют интерфейс и не могут быть нацелены на классы (2) CGLIB, предназначенные для класса, главным образом для генерации подкласса для назначенного класса, охватывающих метод, потому что это наследуемый, следовательно, класс или метод лучше не заявлять в финал

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

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

Основы АОП


Рассмотри некоторую сферическую службу в вакууме (например, 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);

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

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

  • 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 их придётся скомпилировать и «вшить» в основные классы с помощью специального компилятора AJC.

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

Пример использования (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)

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

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

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

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

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

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

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

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

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

В этой части статьи мы подробно рассмотрим другой AOP-фреймворк — Fody, которым мы решили заменить PostSharp. И попробуем ответить на вопрос, что мы приобретаем и что теряем при этом переходе.


Одним из решающим для нас отличием от PostSharp было то, что Fody является бесплатным open source продуктом, использующим MIT-лицензию. Эта лицензия позволяет свободно использовать и модифицировать Fody и в закрытых коммерческих проектах.

Fody и его плагины

Fody, как и PostSharp, выполняет обработку сборок после их компиляции. Схематично, это изображено на рисунке ниже. Этот рисунок аналогичен рисунку для PostSharp, с тем лишь отличием, что PostSharp здесь заменен на Fody.


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

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

Перечислим наиболее примечательные, на наш взгляд, плагины:

· MethodDecorator, на котором мы подробно остановимся чуть позже;

· PropertyChanged, генерирует код для реализации интерфейса INotifyPropertyChanged;

· MethodCache, генерирует код, который кэширует результаты вызовов методов.

Использование Fody

Для того, чтобы использовать Fody в своём проекте достаточно:

1. Добавить в проект NuGet-пакет Fody.

2. Добавить в проект отдельные NuGet-пакеты необходимых плагинов.

3. Добавить в корневой каталог проекта или солюшена файл FodyWeavers.xml такого содержания:


Где WeaverName — это название плагина.

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

Возможности плагина MethodDecorator

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

Поэтому при замене PostSharp недостаточно рассматривать только Fody в отдельности, а необходимо еще подобрать и плагины, реализующие необходимую функциональность.

MethodDecorator — плагин Fody, реализующий функциональность, аналогичную аспекту OnMethodBoundaryAspect у PostSharp, а именно — добавление своей логики в начало и конец метода.

Для примера, приведем код уже знакомого нам атрибута TraceAttribute, адаптированного под MethodDecorator:


Сразу можно отметить, что в новой реализации TraceAttribute сохранены названия методов OnEntry и OnExit, и оба эти метода сохранили свою семантику.

Но на этом сходства заканчиваются. Отметим следующие различия:

1. Для сохранения имени метода используется отдельное поле _methodName, значение которого задается в новом методе Init, специфичным для MethodDecorator.

2. В сигнатурах методов OnEntry, OnExit нет параметров.

А теперь рассмотрим, какой код генерирует MethodDecorator для этого атрибута:


Что можно отметить из приведенного фрагмента кода?

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

Преимуществом такого подхода можно было бы назвать отсутствие необходимости думать о многопоточности при реализации аспекта. В частности, при реализации аспекта можно свободно использовать поля класса для сохранения состояния между вызовами методов Init, OnEntry, OnException. Но при переносе кода адаптированного под PostSharp, где проблемы с многопоточностью уже решены, это не является преимуществом.

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

Другие возможности Fody+MethodDecorator?

Если вспомнить предыдущую часть статьи, помимо OnMethodBoundaryAspect в наших проектах используются и атрибуты, сделанные на основе аспекта MethodInterceptionAspect. Плюс код некоторых аспектов опирался на уникальную возможность PostSharp: инициализацию аспектов во время компиляции через метод.

Если подобная функциональность в плагинах Fody? Ответ: нет.

Для наглядности, приведем табличку, где приведем используемые нами возможности PostSharp и их аналоги в Fody (если они есть):

Возможности PostSharp и аналог в Fody

  1. Аспект OnMethodBoundaryAspect — Аналог в Fody: реализовано плагином MethodDecorator

2. Возможность инициализации на этапе билда (метод CompileTimeInitialize) — Аналога в Fody нет

3. Аспект MethodInterceptionAspect — Аналога в Fody нет

Поэтому при переходе на Fody встает вопрос и о том, каким образом заменить данную функциональность из PostSharp?

Что дальше?

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

Но, и Fody, и MethodDecorator являются продуктами с открытым исходным кодом, поэтому при необходимости их можно довести до ума своими руками, чем мы и занялись.

Перед нами, прежде всего, встали такие проблемы:

1. «Научить» MethodDecorator генерировать более оптимальный, с точки зрения производительности, код, аналогично PostSharp.

2. Придумать, как сделать альтернативу MethodInterceptionAspect для Fody.

В следующей части мы подробно рассмотрим, как реализованы плагины для Fody, заглянем под «капот» плагина MethodDecorator и продемонстрируем решение указанных выше проблем.

В предыдущей статье мы рассмотрели аспектно-ориентированное программирование и его преимущества, и то, чем мы руководствовались при выборе конкретного фреймворка для внедрения AOP в наши проекты. Как было сказано в итогах этого краткого введения, наш выбор остановился на PostSharp.

b_5b0c15eac767c.jpg

Рисунок 1. Процедура билда с использованием PostSharp

Проекту PostSharp уже более 10 лет. За это время он прошел путь от персонального проекта с открытым исходным кодом до полноценного коммерческого продукта. Кратко рассмотрим эволюцию 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

  1. Аспект OnMethodBoundaryAspect ;
  2. Инициализация аспектов во время компиляции через метод CompileTimeInitialize ;
  3. Аспект MethodInterceptionAspect.

Аспект OnMethodBoundaryAspect

Начнем с самого простого аспекта OnMethodBoundaryAspect , позволяющего вставить свою логику в начале и в конце тела метода.

Рассмотрим простейшую реализацию подобного аспекта:

b_5b0c19fc14bce.jpg

  • OnEntry , который вызывается в самом начале выполнения метода;
  • OnExit , который вызывается в самом конце выполнения метода, независимо от того, происходит ли там ошибка или нет.

Для того, чтобы применить аспект к нашему коду, достаточно применить атрибут к методу или классу:

b_5b0c1a093df32.jpg

Схематично PostSharp преобразует код метода для аспектов на основе OnMethodBoundaryAspect таким образом (чуть ниже мы рассмотрим, что генерируется на самом деле):

b_5b0c1a149ca8d.jpg

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

В дополнение к приведенным в примере выше методам, PostSharp поддерживает методы OnSuccess и OnException , для обработки успешного и ошибочного выполнения метода, соответственно.

Наверняка остаются вопросы: откуда берется переменная attribute ? Как информация о методе и его аргументы передаются в аспект? Попробуем ответить на эти вопросы, более детально разобрав код, который на самом деле генерирует PostSharp.

Итак, обработанный PostSharp класс, если произвести дизассемблирование, будет выглядеть приблизительно так:

b_5b0c1a20344c9.jpg

  • PostSharp генерирует отдельный класс (__$Aspects ), содержащий статический конструктор и статические поля для атрибутов и методов. В статическом конструкторе происходит инициализация этих полей. Так как конструктор статический, то создание и инициализация всех полей происходит только один раз.
  • PostSharp генерирует два статических поля для каждого применяемого атрибута. Одно поле содержит ссылку на экземпляр атрибута ( a1 в примере), а другое -- метаинформацию о методе, к которому был применен атрибут ( m1 ).
  • Информация о методе получается c использованием механизма Reflection, а конкретно метода MethodBase.GetMethodFromHandle , куда передаются ссылки на метод и тип, где определен метод.
  • Как создается экземпляр атрибута a1 в приведенном коде не демонстрируется. Особенность PostSharp в том, что он создает объекты атрибутов во время компиляции. Более подробно об этом будет написано при описании метода CompileTimeInitialize ниже.
  • Однократная инициализация атрибута выполняется через отдельный метод: RuntimeInitialize .
  • При вставке своего кода PostSharp использует сгенерированные статические поля, для которых вызывает соответствующие методы ( OnEntry , OnExit и т.д.), передавая контекст выполнения в параметре inArgs .

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

Рассмотрим другую важную для нас возможность PostSharp.

Метод CompileTimeInitialize

Приведенный выше пример реализации TraceAttribute можно было бы переписать следующим образом:

b_5b0c1a3b64e41.jpg

Метод CompileTimeInitialize вызывается только один раз и, что самое главное, он вызывается во время обработки PostSharp сборки, т.е. на этапе билда.

Дело в том, что все экземпляры атрибутов PostSharp создает на этапе компиляции, позволяя их инициализировать через этот метод. Далее, PostSharp сериализует все объекты и сохраняет их сериализованное представление внутри ресурсов сборки.

Аспект MethodInterceptionAspect

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

Тем самым, атрибуты на основе MethodInterceptionAspect позволяют реализовать намного более нетривиальную логику.

  1. Упрощение UI-кода, работающего в многопоточном режиме.
  2. Аутентификация пользователей, включающая в себя и нетривиальную логику, вроде проверки действительности и ограничений лицензии того или иного продукта.

Остановимся на более простой задаче по UI. В десктопной части наших проектов используется технология WinForms, и одной из особенностей данной технологии является обязательное требование выполнять обращения к GUI-контролам в отдельном GUI-потоке. И такое требование может принести много головной боли при реализации достаточно сложного UI с многопоточностью.

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

Итак, рассмотрим реализацию такого атрибута:

b_5b0c1a58d8cd4.jpg

b_5b0c1a64238ce.jpg

В данном примере свойство args.Proceed является делегатом метода, содержащего логику декорируемого метода.

Нужное поведение, а именно вызов метода в GUI-потоке, достигается за счет генерации подобного кода:

b_5b0c1a6d12f12.jpg

В примере опущен код, генерируемый для инициализации полей aspect и method : он аналогичен коду, генерируемому для OnMethodBoundaryAspect .

Как видно из примера, PostSharp копирует тело декорируемого метода в новый сгенерированный метод и полностью заменяет код исходного метода. В декорируемый метод добавляется вызов метода атрибута, в который передается делегат, указывающий на сгенерированный метод.

Почему не PostSharp?

  • Сквозная трассировка через атрибут, подобный атрибуту TraceAttribute из этой статьи;
  • Вызов методов GUI-контролов в GUI-потоке через атрибут OnGUIThreadAttribute ;
  • Аудит и аутентификация, с использованием атрибутов на основе базового атрибута OnMethodBoundaryAspect .

Из разбора кода, генерируемого PostSharp, можно сделать, что он ориентирован на производительность, достаточно качественен и хорошо продуман.

Вроде бы все хорошо, но почему же мы решили перейти с PostSharp на что-то другое?

Среди наших заказчиков есть и российские, продукты которых должны проходить сертификацию на отсутствие в них недокументированных возможностей по второму уровню (НДВ-2) в Федеральной службе по техническому и экспортному контролю (ФСТЭК). Это означает, что некоторые наши проекты должны проверяться на отсутствие не декларированных возможностей. Для сертификации по НДВ-2 в продукт нельзя включать компоненты без исходных кодов. При этом должна иметься возможность собрать проект на основе предоставленных исходников [см ссылку 2 в конце статьи].

Здесь и возникает проблема с PostSharp. Несмотря на то, что PostSharp по сути не является компонентом, при сборке он необходим. При этом PostSharp не сертифицирован, поэтому включать в поставку проекта PostSharp без исходных кодов нельзя. Начиная с версии 2.0, PostSharp является продуктом с закрытым исходным кодом и платной лицензией, которая не включает в себя исходный код продукта.

Поэтому, в итоге, мы решили отказаться от использования PostSharp в наших проектах в пользу Open Source-решения, которое для этого пришлось прилично переделать. О деталях миграции на новый AOP-фреймворк, сложностях и проблемах на этом пути, мы напишем в следующей части.

Читайте также: