Feel Good.

14 апреля 2011

AOP средствами Unity 2.0

В этой статье рассмотрим простой пример реализации AOP (Aspect-Oriented Programming) средствами Unity 2.0. Итак, Unity представляет собой DI/IoC контейнер, с возможностью вносить в него новую функциональность или поведение через стандартные или пользовательские расширения (Extentions).Благодаря этой возможности Unity, мы можем написать свое или воспользоваться уже готовым расширением и реализовать AOP в нашем .NET приложении. Стандартный подход при реализации AOP средствами Unity является использование перехватчика (Microsoft.Practices.Unity.Interception) вызова.
Unity 2.0 предоставляет нам три вида перехвата:
  1. InterfaceInterceptor для перехвата методов интерфейса
  2. VirtualMethodInterceptor для перехвата виртуальных методов
  3. TransparentProxyInterceptor для перехвата методов у объектов типа MarshalByRefObject
Принцип работы перехватчика очень простой. Когда мы делаем resolve-запрос к Unity контейнеру мы получаем динамически сгенерированный прокси-объект, делегирующий искомый тип. Функционально прокси устроен следующим образом: ПЕРЕД тем и ПОСЛЕ того, как прокси передаст вызов на искомый объект, он выполнит код перехватчика (на практике обычно это pipe из нескольких обработчиков, реализующих интерфейс ICallHandler), передав ему все необходимые параметры вызова. Этого вполне достаточно для реализации AOP поведения.

Для простоты продемонстрирую это на примере: предположим нам необходимо реализовать транзакционный метод (метод помеченный атрибутом [TransactionMethod] будет автоматически выполняться в транзакции, см атрибут [Transaction] в Spring.NET).
Рассмотрим интерфейс и его имплементацию. В интерфейсе у нас имеется метод, декларативно(!) помеченный как транзакционный. Имплементация по сути не важна.

public interface IAccountService

{

    // Хотим чтобы метод Withdraw выполнялся под транзакцией,

    // поэтому пометим его специальным атрибутом.

    [TransactionMethod()]

    void Withdraw(decimal amount);

}

 

public class AccountService : IAccountService

{

    public void Withdraw(decimal amount)

    {

        if (amount < 0 || amount > 1000)

            throw new ArgumentOutOfRangeException("amount");

    }

}


Собственно атрибут. Видно, что он наследуется от HandlerAttribute, именно атрибуты такого типа по-умолчанию и обрабатываются перехватчиком.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]

public class TransactionMethodAttribute : HandlerAttribute

{

    public override ICallHandler CreateHandler(IUnityContainer container)

    {

        // Перехватчик для каждого атрибута HandlerAttribute,

        // вызывает метод CreateHandler, который возвращает

        // конкретный обработчик ICallHandler.

        return new TransactionMethodCallHandler();

    }

}


Вся логика сосредоточена в обработчике вызова ICallHandler.Invoke:

public class TransactionMethodCallHandler : ICallHandler

{

    // Главный метод обработчика ICallHandler.

 

    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)

    {

        IMethodReturn result;

 

        // открываем транзакцию

        using (TransactionScope transaction = new TransactionScope())

        {               

            result = getNext()(input, getNext);

 

            if (result.Exception == null)

            {

                // исключений не возникло, завершим транзакцию

                transaction.Complete();

            }

        }

        return result;

    }

 

    public int Order { get; set; }

}


Продемонстрируем наш пример:

static void Main(string[] args)

{

    IUnityContainer container = new UnityContainer();

 

    // Настроим контейнер.

    UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");

    section.Configure(container);

 

 

    // Запросим у контейнера IAccountService.

    IAccountService service = container.Resolve<IAccountService>();

 

    // Полученный service - прокси-класс делегирующий AccountService.

 

    // Если мы попробуем узнать тип объекта service, то мы получим нечто вида:

    // DynamicModule.ns.Wrapped_IAccountService_af75f3474a9d4de2b1fe6aa1d26a8e36

 

    service.Withdraw(33); // транзакция пройдет успешно

    service.Withdraw(-6); // транзакция откатится

}


Чтобы все это заработало, осталось настроить Unity контейнер через config файл. Укажем правило разрешения типа (mapping) и тип перехвата InterfaceInterceptor:

<?xml version="1.0" encoding="utf-8" ?>

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">

 

  <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"></sectionExtension>

 

  <!-- Заведем псевдонимы для известных типов -->

  <alias alias="IAccountService" type="UnityTest.IAccountService, UnityTest" />

  <alias alias="AccountService" type="UnityTest.AccountService, UnityTest" />

 

  <container>

 

    <extension type="Interception"/>

 

    <register type="IAccountService" mapTo="AccountService">

      <interceptor type="InterfaceInterceptor"/>

      <policyInjection/>

    </register>

 

  </container>

 

</unity>


PS: Пример можно доработать, добавив дополнительные параметры настройки транзакции, например:

[TransactionMethod(TransactionScopeOption.Suppress)]



Ссылки:
  1. AOP времени исполнения в Unity
  2. AOP - Interception with Unity 2.0
  3. AOP in .NET with Unity Interception Model
  4. Unity and AOP in enterprise library

11 комментариев:

  1. Спасибо за статью. У меня есть вопрос с перфомансом, насколько он ниже, если сранвивать с тем же PostSharp-ом?
    Так как я понял, Unity генерит прокси на лету, в отличии от PostSharp-а, который делает вставки кода в момент компиляции.

    ОтветитьУдалить
  2. @Sergey Litvinov
    Performance я не сравнивал, так как не пользуюсь PostSharp (он вроде бы стал платным). Вероятно PostSharp будет быстрее, это все-таки заранее сгенерированный код, хотя возможно Unity делает предварительною генерацию проксей перед пуском приложения, но я придерживаюсь реализации AOP на Unity из-за то, что мы вдобавок к AOP получаем еще и DI/IoC контейнер.

    Кстати вот еще хорошая ссылка:
    http://ayende.com/Blog/archive/2007/07/02/7-Approaches-for-AOP-in-.Net.aspx

    ОтветитьУдалить
  3. Заглянул в исходники Unity, и выяснил что на время выполнения, Unity сохраняет сгенерированные прокси во временную сборку Unity_ILEmit_InterfaceProxies используя System.Reflection.Emit (http://unity.codeplex.com/SourceControl/changeset/view/63122#427069)

    ОтветитьУдалить
  4. http://gandjustas.blogspot.com/2011/04/unity-20-interception.html

    ОтветитьУдалить
  5. @gandjustas
    Кстати, если мы настроим перехватчик в xml-файле, а не в коде, то мы без модификации последнего сможем подключать/отключать его, это хорошо распространяется, например, на диагностические перехватчики не влияющие на логику системы.

    ОтветитьУдалить
  6. Да, зачастую такие вещи как логирование и Performance Counters лучше устанавливать в конфиге, а то что касается БЛ (транзакционность, кеширование, протаскивание контекста) иметь в коде.

    ОтветитьУдалить
  7. "В Лиспе, если охота аспектно-ориентированного программирования, нужно лишь настругать немного макрокоманд, и готово. В Java, нужен Грегор Кичалес, создающий новую фирму, и месяцы и годы попыток заставить всё работать." П.Норвиг
    Вижу, в C# -- то же мракобесие, что и у нас на Java. Кажется, мы когда-то выбрали синюю пилюлю вместо красной...

    ОтветитьУдалить
  8. @Dmitry
    AOP'ом должен управлять framework, в .NET он пока отсутствует, поэтому приходиться заполнить пробел подручными средствами, например Unity. Но .NET же не стоит на месте.

    ОтветитьУдалить
  9. Да, бесспорно, если нет ничего лучше, то приходится юзать то, что есть. На работе я с java мучаюсь точно так же, как и ты с C#-ом, из-за бедных средств обобщенного программирования в языке. Что C#, что Java -- языки слишком низкоуровневые и невыразительные. Если бы у них были средства нормального метапрограммирования, то не пришлось бы юзать AOP-фреймворки.
    Воспоминание Кенни Тилтона:
    Я помню как Грегор Кичалес на ILC 2003 [Международная конференция по Лиспу], рассказывая об AspectJ безмолвной толпе, остановился, затем жалобно добавил «Когда я показал это Java-программистам они встали и заапплодировали».

    ОтветитьУдалить
  10. а не подскажете чем можно перехватывать запросы в unity web player

    ОтветитьУдалить
  11. К сожалению я не знаком с Unity (которая game engine)

    ОтветитьУдалить