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

Определение того, где применять

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

alt text

Конечно, мы можем просто перечислить все точки присоединения, к которым должен быть применен какой-то конкретный совет. Примерно так:

Apply RetryInterceptor At
CalculatorWebService.Add
CalculatorWebService.Divide
...

Однако это совсем не то, что мы хотим, т.к. в больших приложениях это приведет к заметным трудностям в перечислении каждой точки присоединения. Хотелось бы чего-то подобного:

Apply RetryInterceptor At
All Service Methods

Выглядит гораздо лучше. Но как это описать? Это похоже на SQL-запрос для выборки записей из таблицы. То, с чем мы имеем дело, - это множество наших точек присоединения и наше условие "where", которое будет описывать характеристики тех точек присоединения, которые мы хотели бы выбрать. Применительно к нашему примеру мы могли бы написать следующее:

Apply RetryInterceptor At
ClassName LIKE '*Service' AND MethodName LIKE '*'

В терминах AOP, такое выражение SELECT, отбирающее из всего множества точек присоединения некое их подмножество, называется pointcut (с англ. некоторая выборка из всех точек).

alt text

Применение советов к pointcut'ам: введение в Object Post Processor'ы

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

<object name="Alice" type="Spring.Objects.TestObject">           
<property name="age" value="31"/>
</object>

<object name="Bob" type="Spring.Objects.TestObject">
<property name="age" value="33"/>
<property name="spouse" ref="Alice" />
</object>

Используя подобное описание объектов, мы можем построить целый граф. Т.к. все эти объекты создаются под контролем Spring'а, то мы можем воспользоваться одной из его точек расширения, чтобы что-либо сделать со свежесозданным объектом. Данная точка расширения называется "object post-processing". Каждый раз, когда создается новый объект (под контролем Spring'а, разумеется), Spring передает его на обработку всем описанным в конфигурации объектам, реализующим интерфейс IObjectPostProcessor:

alt text

Это как раз то, что мы искали! Мы сможем реализовать свой собственный IObjectPostProcessor и обернуть объекты нужных нам типов в прокси. Простой пример подобной реализации с использованием функции, определяющей, удовлетворяет ли объект критерию, приведен ниже:

public class CustomAutoProxyCreator : IObjectPostProcessor
{
private readonly Func<object, string, bool> matchesPointcut;
private readonly IMethodInterceptor[] advices;

public CustomAutoProxyCreator(Func<object, string, bool> matchesPointcut,
                              params IMethodInterceptor[] advices)
{
  this.matchesPointcut = matchesPointcut;
  this.advices = advices;
}

public object PostProcessBeforeInitialization(object instance, string objectName)
{
  return instance;
}

public object PostProcessAfterInitialization(object instance, string objectName)
{
  if (matchesPointcut(instance, objectName))
  {
    ProxyFactory proxyFactory = new ProxyFactory(instance);
    foreach(var advice in advices)
    {
      proxyFactory.AddAdvice(advice);
    }
    return proxyFactory.GetProxy();
  }
  return instance;
}
}

И теперь мы можем добавить CustomAutoProxyCreator в конфигурацию, чтобы добавить операцию повтора и кэширование результата ко всем объектам, чье конфигурационное имя заканчивается на "Service":

[Configuration]
public class AutoProxyDemoConfig
{
public CalculatorWebService TheCalculatorService()
{
  return new CalculatorWebService();
}

public CustomAutoProxyCreator AutoProxyCreator()
{
  return new CustomAutoProxyCreator(
      (instance, name) => name.EndsWith("Service"),
      new CacheInterceptor(), new RetryInterceptor()
  );
}
}

Примечание: про атрибут Configuration можно почитать здесь.

Применение и посторное использование AutoProxy-особенности Spring.NET - класса DefaultAdvisorAutoProxyCreator

Конечно вы не должны создавать свою собственную реализацию IObjectPostProcessor для автоматического создания AOP прокси. В Spring.NET уже есть заранее определенные реализации постпроцессоров, которые позволяют вам выбрать различные стратегии для автоматической генерации прокси. DefaultAdvisorAutoProxyCreator (DAAPC) является наиболее гибкой и мощной из этих реализаций.

За счет чего DAAPC настолько хорош? Вспомните, как мы говорили о pointcut'ах и советах. Так вот DAAPC позволяет с легкостью добавлять их комбинации в конфигурацию контейнера и автоматически использует их для реализации всего того, чего мы добивались. Для этой цели в Spring.NET существует специальный тип объектов, называемый советник (от англ. advisor). Это ничто иное как комбинация pointcut'а и советов, которые должны быть к нему применены:

alt text

Чтобы использовать DAAPC добавьте его и список необходимых советников в конфигурацию контейнера. DAAPC автоматически найдет все элементы из этого списка и применит их к вашим объектам. Следующий пример продемонстрирует, как применять советы, следуя этой технике:

[Configuration]
public class DefaultAdvisorAutoProxyDemoConfig
{
public CalculatorWebService TheCalculatorService()
{
  return new CalculatorWebService();
}

public DefaultAdvisorAutoProxyCreator AutoProxyCreator()
{
  return new DefaultAdvisorAutoProxyCreator();
}

public DefaultPointcutAdvisor CacheServiceCallResultsAdvisor()
{
  return new DefaultPointcutAdvisor(
    new SdkRegularExpressionMethodPointcut(@".*Service\.*"),
    new CacheInterceptor()
  );
}

public DefaultPointcutAdvisor RetryServiceCallResultsAdvisor()
{
  return new DefaultPointcutAdvisor(
    new SdkRegularExpressionMethodPointcut(@".*Service\.*"),
    new RetryInterceptor()
  );
}
}

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

Приближаемся к завершению...

Чумовое путешествие! Мы начали с разделения поведения на отдельные классы, используя шаблон "Декоратор". Введя перехватчики, мы сделали эти классы поведения повторно используемыми. Когда мы узнали о ProxyFactory, у нас отпала необходимость создавать собственные proxy классы. В конце концов, используя pointcut'ы и спринговскую технологию AutoProxy, мы научились с легкостью добавлять советы на любое подмножество объектов конфигурации. Следуя простой логике и пошаговому рефакторингу, мы пришли к этой мистической технике, называемой аспекто-ориентированное программирование (от англ. Aspect-Oriented Programming), и доказали, что ничего сверхестественного в ней нет. Так же это доказывает, что ООП и AOP не являются взаимоисключающими, а замечательно работают вместе.

Как и ранее, вы можете скачать исходный код к данной статье.