Статьи

Мета-программирование Java

В последние пару лет, если вы упомянули термин « метапрограммирование» , у людей оживились … и они начали искать Руби. Это честно; Ruby делает множество концепций метапрограммирования очень и очень простым. Однако, это не означает , что вы не можете делать какие — либо мета-программирование на Java; Вы просто более ограничены и нуждаетесь в гораздо большей инфраструктуре.

Tapestry 5, как веб-фреймворк, так и базовый контейнер Inversion of Control, изобилует опциями метапрограммирования. Давайте поговорим об одном из самых универсальных: о Thunk .

Thunks и Laziness

Thunk — это заполнитель для значения, которое будет вычислено по мере необходимости. Язык программирования Haskell прекрасно их использует; thunks — это суть ленивого программирования: каждый thunk представляет собой набор параметров для функции 1 и самой функции.

Результатом этого является то, что когда вы видите вызов функции (или другое выражение) в коде на Haskell, то, что действительно происходит, это то, что поток вызова этой функции создается для захвата значений, которые должны быть переданы (некоторые из которых могут сами по себе) быть буйством других выражений). Только когда значение необходимо, когда результат выражения используется в каком-то другом выражении, которое оценивается, сам блок обрабатывается; функция вызывается, возвращаемое значение кэшируется в thunk и возвращается. Это делает порядок, в котором все происходит в Хаскеле, очень трудно предсказать, особенно со стороны. Из-за thunks, алгоритмы, которые выглядят хвостовыми рекурсивными, не являются (рекурсивный вызов — просто еще один thunk, оцениваемый последовательно) Кроме того, алгоритмы, которые кажутся бесконечными, не являются:thunks гарантирует, что справедливые значения, которые действительно необходимы, когда-либо вычисляются.

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

Другие языки имеют эту функцию; Clojure отражает свое наследие Lisp в том, что почти все работает с точки зрения доступа, итерации и преобразования коллекций … и все эти операции сбора также ленивы. В отличие от Haskell, это скорее функция тщательно созданной стандартной библиотеки, чем прямой ответвление языка, но конечный результат довольно похож.

Но что произойдет, если вы захотите выполнить некоторые из этих функций (например, ленивая оценка) в рамках жестких ограничений стандартной Java? Вот когда вам нужно проявить творческий подход!

Thunks в Гобелен 5

Tapestry 5 использует громоотводы во многих разных местах; наиболее распространенным является использование прокси для сервисов Tapestry 5 IoC. В Tapestry 5 каждый сервис имеет интерфейс 2 . Давайте взглянем на типичный сервис в Tapestry 5, чтобы проиллюстрировать концепцию набираемого текста.

Листинг 1: ComponentMessagesSource.java

public interface ComponentMessagesSource
{
Messages getMessages(ComponentModel componentModel, Locale locale);
InvalidationEventHub getInvalidationEventHub();
}

Цель службы ComponentMessagesSource — предоставить объект Messages, представляющий каталог сообщений конкретного компонента. Это является частью поддержки локализации Tapestry: каждая страница и компонент имеют легкий доступ к своему собственному пакету сообщений, который включает в себя сообщения, унаследованные от базовых компонентов и из глобального каталога сообщений.

Основной принцип Tapestry 5 заключается в том, что создание сервисов лениво: сервисы создаются только по мере необходимости. Что значит «по необходимости»? Это означает, что в первый раз любой метод службы вызывается. Этот вид ленивого воплощения достигнут с помощью thunks. Таким образом, для службы, такой как ComponentMessagesSource, будет класс, похожий на ComponentMessagesSourceThunk, для обработки отложенных экземпляров:

Листинг 2: ComponentMessagesSourceThunk.java

public interface ComponentMessagesSourceThunk implements ComponentMessagesSource
{
private final ObjectCreator creator;

public ComponentMessagesSourceThunk(ObjectCreator creator) { this.creator = creator; }

private ComponentMessagesSourceThunk delegate() { return (ComponentMessagesSourceThunk) creator.createObject(); }

public Messages getMessages(ComponentModel componentModel, Locale locale)
{
return delegate().getMessages(componentModel, locale);
}

public InvalidationEventHub getInvalidationEventHub()
{
return delegate().getInvalidationEventHub();
}
}

 

Вы не найдете вышеупомянутый класс в исходном коде Tapestry: он создается на лету Tapestry. Это здорово, потому что я знаю, что не хотел бы предоставлять интерфейс сервиса, реализацию сервиса
и класс thunk для каждого сервиса; интерфейс и реализация уже достаточно! Одна из причин, по которой Tapestry требует наличия сервисных интерфейсов, — это поддержка автоматического создания групповых соединений или других прокси вокруг интерфейса.

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

Дело в том, что для любого интерфейса вы можете создать типизированный блок, который может заменить реальный объект, скрывая жизненный цикл реального объекта: он создается по требованию ObjectCreator. Код, который использует thunk, не может отличить thunk от реальных объектов … thunk реализует все методы интерфейса и выполняет правильное поведение при вызове этих методов.

Создание Thunks динамически

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

Листинг 3: ThunkCreator.java

public interface ThunkCreator
{
/**
* Creates a Thunk of the given proxy type.
*
* @param proxyType type of object to create (must be an interface)
* @param objectCreator provides an instance of the same type on demand (may be invoked multiple times)
* @param description to be returned from the thunk's toString() method
* @param <T> type of thunk
* @return thunk of given type
*/
<T> T createThunk(Class<T> proxyType, ObjectCreator objectCreator, String description);
}

Помните, что это просто автоматизированный способ создания экземпляров классов, подобных ComponentMessagesSourceThunk. Простая реализация этого сервиса возможна с использованием JDK Proxies:

Листинг 4: ThunkCreatorImpl.java

public class ThunkCreatorImpl implements ThunkCreator
{
public <T> T createThunk(Class<T> proxyType, final ObjectCreator objectCreator, final String description)
{
InvocationHandler handler = new InvocationHandler()
{
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
if (method.getName().equals("toString") && method.getParameterTypes().length == 0)
return description;

return method.invoke(objectCreator.createObject(), args);
}
};

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[] { proxyType },
handler);

return proxyType.cast(proxy);
}
}

Прокси JDK были введены еще в JDK 1.3 и вызвали настоящий всплеск активности, потому что они невероятно полезны. Вызов Proxy.newProxyInstance () создаст объект, соответствующий предоставленным интерфейсам (здесь указывается как параметр proxyType). Каждый вызов метода направляется через один объект InvocationHandler. InvocationHandler просто перенаправляет вызовы метода к объекту, возвращенному из objectCreator.createObject ().

Реализация Tapestry в ThunkCreator использует библиотеку манипулирования байт-кодом Javassist для генерации пользовательского класса во время выполнения. Сгенерированный класс намного ближе к примеру CompnentMessagesSourceThunk; он не использует прокси JDK или рефлексию. Это означает, что компилятор Hotspot в Java может лучше оптимизировать код. В действительности вам будет сложно определить разницу в производительности, если вы не используете эти громкоговорители в очень узкой петле.

Отлично до сих пор; Теперь давайте подумаем о том, как мы могли бы использовать это по-другому. Что если у вас есть сервис, который возвращает объект, который дорого построить, а может даже не привыкнуть? Примером этого в Tapestry является объект Messages, полученный из службы ComponentMessagesSource. Создание экземпляра Messages для компонента включает в себя большую часть поиска пути к файлам свойств не только для компонента, но и для его базового класса, а также для пакетов сообщений всего приложения. Это означает, что много ввода-вывода и много блокировок, ожидая, пока диск не подтянется. Во многих случаях эти объекты Messages внедряются в компоненты, но не используются сразу. С точки зрения ускорения получения разметки в браузере пользователя, избегать всех этих поисков файлов и их чтения до тех пор, пока это абсолютно не станет необходимым, — ощутимый выигрыш.

Наша цель — перехватить вызов ComponentMessagesSource.getMessages () и записать параметры в метод. Вместо того, чтобы вызывать метод, мы хотим вернуть thunk, который инкапсулирует вызов метода. Здесь мы действительно можем начать говорить о метапрограммировании , а не только о программировании: мы не собираемся менять реализацию службы ComponentMessagesSource для достижения этой цели, мы собираемся метапрограммировать службу. Это ключевой момент: сервис Tapestry — это сумма его интерфейса, его реализации и всех других частей, предоставляемых Tapestry . Мы можем использовать Tapestry для улучшения поведения сервиса без изменения реализации самого сервиса.

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

Выполнить все это на удивление легко … учитывая инфраструктуру, которую уже обеспечивает Tapestry 5 IoC.

Ленивый Совет

Цель с ленивым советом состоит в том, что вызов метода в службе приводит к короткому замыканию вызова метода: возвращается thunk, который заменяет возвращаемое значение метода. Вызов метода для thunk вызовет фактический метод службы, а затем повторно вызовет метод для фактического значения, возвращенного методом.

Изображение 1: Lazy Advice Thunk /

Это показано на рисунке 1. Метод обслуживания представлен синей линией. Совет перехватывает вызов (запоминание параметров метода) и возвращает thunk. Позже, вызывающая сторона вызывает метод на панели (зеленая линия). Thunk вызовет метод службы, используя сохраненные параметры (это ленивая часть), а затем повторно вызовет метод для возвращенного значения.

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

Теперь мы знаем, как будет выглядеть решение … но как мы можем сделать это на самом деле? Как нам получить «там», чтобы посоветовать методы обслуживания?

Консультирование Методы обслуживания

Инверсия контейнера управления Tapestry организована вокруг модулей: классов, определяющих сервисы. Это в отличие от Spring , который использует подробные XML-файлы. Tapestry использует соглашение об именах, чтобы выяснить, какие методы класса модуля делают что. Методы, чье имя начинается с «build», определяют сервисы (и в конечном итоге используются для их создания). Другие префиксы имен методов имеют разные значения.

Имена методов модуля с префиксом «advise» действуют как ловушка для ограниченного количества аспектно-ориентированного программирования . Tapestry предоставляет простой способ давать советы по вызовам методов … более навязчивая система, такая как AspectJ, может легко перехватывать доступ к полям или даже построение классов и имеет больше возможностей для ограничения объема рекомендаций, так что она применяется только к вызовы в определенных классах или пакетах. Конечно, это работает, существенно переписывая байт-код ваших классов, и контейнер IoC Tapestry стремится к легкому прикосновению.

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

В листинге 5 показан совет метода для службы ComponentMessagesSource.

Листинг 5: TapestryModule.java

    @Match("ComponentMessagesSource")
public static void adviseLazy(LazyAdvisor advisor, MethodAdviceReceiver receiver)
{
advisor.addLazyMethodInvocationAdvice(receiver);
}

Этот метод используется для уведомления конкретной службы, идентифицируемой уникальным идентификатором службы, здесь «ComponentMessagesSource». Метод советника может посоветовать множество разных услуг; мы могли бы использовать глобальные имена или регулярные выражения для соответствия более широкому спектру услуг. Метод советника получает MethodAdviceReceiver в качестве параметра; дополнительные параметры вводятся сервисами. Цель классов модулей состоит в том, чтобы содержать минимальный объем кода, поэтому имеет смысл перенести реальную работу в сервис, особенно потому, что так просто внедрить сервисы непосредственно в метод советника.

Сервис LazyAdvisor, встроенный в Tapestry, выполняет большую часть работы:

Список 6: LazyAdvisorImpl.java

public class LazyAdvisorImpl implements LazyAdvisor
{
private final ThunkCreator thunkCreator;

public LazyAdvisorImpl(ThunkCreator thunkCreator)
{
this.thunkCreator = thunkCreator;
}

public void addLazyMethodInvocationAdvice(MethodAdviceReceiver methodAdviceReceiver)
{
for (Method m : methodAdviceReceiver.getInterface().getMethods())
{
if (filter(m))
addAdvice(m, methodAdviceReceiver);
}
}

private void addAdvice(Method method, MethodAdviceReceiver receiver)
{
final Class thunkType = method.getReturnType();

final String description = String.format("<%s Thunk for %s>",
thunkType.getName(),
InternalUtils.asString(method));

MethodAdvice advice = new MethodAdvice()
{
/**
* When the method is invoked, we don't immediately proceed. Intead, we return a thunk instance
* that defers its behavior to the lazily invoked invocation.
*/
public void advise(final Invocation invocation)
{
ObjectCreator deferred = new ObjectCreator()
{
public Object createObject()
{
invocation.proceed();

return invocation.getResult();
}
};

ObjectCreator cachingObjectCreator = new CachingObjectCreator(deferred);

Object thunk = thunkCreator.createThunk(thunkType, cachingObjectCreator, description);

invocation.overrideResult(thunk);
}
};

receiver.adviseMethod(method, advice);
}

private boolean filter(Method method)
{
if (method.getAnnotation(NotLazy.class) != null) return false;

if (!method.getReturnType().isInterface()) return false;

for (Class extype : method.getExceptionTypes())
{
if (!RuntimeException.class.isAssignableFrom(extype)) return false;
}

return true;
}
}

Ядро службы LazyAdvisor находится в методе addAdvice (). MethodAdvice внутренний класс определен; Интерфейс MethodAdvice имеет только один метод, advise (). Методу advise () будет передан Invocation , представляющий вызываемый метод. Invocation захватывает переданные параметры, а также возвращаемое значение или любые проверенные исключения, которые генерируются. Вызов метода continue () продолжается до исходного метода службы 3 .

В этот момент thunk инкапсулирует исходный вызов метода; у нас даже есть объект для этого: экземпляр Invocation, первоначально переданный методу advise (). Вызов любого метода в thunk приведет к срабатыванию метода ObjectCreator.createObject (): здесь мы наконец-то вызываем continue () и возвращаем значение для лениво вызываемого метода.

Другое использование для Thunks

По сути, такой подход дает вам возможность контролировать контекст, в котором выполняется метод: выполняется ли он прямо сейчас или только при необходимости? Это всего лишь маленький переход от выполнения метода в фоновом потоке. Фактически, Tapestry включает в себя сервис ParellelExecutor, который можно использовать только для этого.

Вывод

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

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

  • Мы можем создать Thunk вокруг ObjectCreator, чтобы отложить создание объекта
  • Мы можем захватить вызов метода и преобразовать его в ObjectCreator и ленивый thunk
  • Мы можем посоветовать метод без изменения фактической реализации, чтобы обеспечить желаемую лень
  • Tapestry может вызывать метод советника нашего модуля при создании службы ComponentMessagesSource
  • Мы можем внедрить услуги, которые делают консультирование прямо в методы советника

Сноски

1 На самом деле, все функции в Haskell принимают ровно один параметр, который является потрясающим и не имеет отношения к обсуждению.

2 Сервисы могут основываться на классах, а не на интерфейсах, но тогда вы потеряете многие из этих интерфейсных функций, таких как ленивые прокси.

3 Или, если метод был посоветован несколько раз, вызов метода continue () может вызвать следующий совет. Например, вы могли добавить совет к методу для регистрации входа и выхода метода, а также для управления транзакциями базы данных, а также для отложенной оценки.

С http://tapestryjava.blogspot.com