В предыдущей статье из данной серии я рассказал, как можно использовать спринговский класс ProxyFactory, чтобы обернуть нужный вам объект в прокси и добавить к нему перехватчики для расширения функциональности ваших методов без изменения кода этих методов. Тем не менее это довольно объемная работа - оборачивать каждый объект в прокси вручную.
Определение того, где применять
То, что нам нужно - это более автоматизированный подход, где мы могли бы просто описать объекты, или даже лучше - методы, поведение которых мы бы хотели дополнить. Перехватчик ("совет" в терминах AOP) описывает то, что нужно делать (например, операция повтора), так что нам нужен способ описания, к чему применять дополнительное поведение. В терминах AOP каждый метод, к которому может быть применен перехватчик, называется точкой присоединения (от англ. joinpoint).
Конечно, мы можем просто перечислить все точки присоединения, к которым должен быть применен какой-то конкретный совет. Примерно так:
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 (с англ. некоторая выборка из всех точек).
Применение советов к 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:
Это как раз то, что мы искали! Мы сможем реализовать свой собственный 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'а и советов, которые должны быть к нему применены:
Чтобы использовать 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 не являются взаимоисключающими, а замечательно работают вместе.
Как и ранее, вы можете скачать исходный код к данной статье.