Статьи

Сделать агентов, а не рамки

С момента своего появления аннотации Java стали неотъемлемой частью API-интерфейсов более крупных платформ приложений. Хорошими примерами таких API-интерфейсов являются Spring или Hibernate, где добавление нескольких строк кода аннотации реализует довольно сложную программную логику. И хотя можно спорить о недостатках этих конкретных API, большинство разработчиков согласятся, что эта форма декларативного программирования весьма выразительна при правильном использовании. Однако лишь немногие разработчики предпочитают реализовывать API-интерфейсы на основе аннотаций для своих собственных платформ или промежуточного программного обеспечения приложений, главным образом потому, что их считают сложными для реализации. В следующей статье я хочу убедить вас, что такие API, напротив, довольно тривиальны для реализации и, используя правильные инструменты, не требуют каких-либо специальных знаний о встроенных функциях Java.

Одна проблема, которая становится совершенно очевидной при реализации API на основе аннотаций, состоит в том, что аннотации не обрабатываются исполняющей средой выполнения Java. Как следствие, невозможно присвоить конкретное значение данной пользовательской аннотации. Например, предположим, что мы хотели определить аннотацию @Log которую мы хотим предоставить для простой регистрации каждого вызова аннотированного метода:

1
2
3
4
5
6
class Service {
  @Log
  void doSomething() {
    // do something ...
  }
}

Поскольку аннотация @Log не способна выполнять программную логику одним своим существованием, пользователь аннотации может выполнить запрошенную запись в журнал. Очевидно, что это делает аннотацию практически бесполезной, поскольку мы не можем вызвать метод doSomething и ожидать, что в нашем журнале появится соответствующий оператор. Пока что аннотация служит только маркером, не внося никакой логики в программу.

Преодоление разрыва

Чтобы преодолеть это явное ограничение, многие платформы, основанные на аннотациях, используют подклассы в сочетании с переопределением методов для реализации логики, связанной с конкретной аннотацией. Это обычно называют инструментальными средствами подкласса. Для предложенной аннотации @Log инструментарий подкласса приведет к созданию класса, подобного следующему LoggingService :

1
2
3
4
5
6
7
class LoggingService extends Service {
  @Override
  void doSomething() {
    Logger.log("doSomething() was called");
    super.doSomething();
  }
}

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

Но это масштабируется?

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

По этой причине фреймворки, такие как Spring или Hibernate, используют фабрики объектов и не допускают прямого создания экземпляров объектов, которые считаются частью их логики фреймворка. В Spring создание объектов фабрикой происходит естественным образом, поскольку все объекты Spring уже являются управляемыми компонентами, которые в первую очередь должны быть созданы фреймворком. Точно так же большинство объектов Hibernate создаются в результате запроса и, следовательно, не создаются явно. Однако, например, при сохранении экземпляра объекта, который еще не представлен в базе данных, пользователь Hibernate должен заменить недавно сохраненный экземпляр экземпляром, который возвращается из Hibernate после хранения. Если посмотреть на вопросы о Hibernate, игнорирование этой замены уже приводит к общей ошибке новичка. Кроме того, благодаря этим фабрикам инструментарий подкласса оказывается в основном прозрачным для пользователя платформы, потому что система типов Java подразумевает, что подкласс может заменить любой из его суперклассов. Следовательно, экземпляр LoggingService может использоваться везде, где пользователь ожидает экземпляр пользовательского класса Service .

К сожалению, этот одобренный метод фабрики экземпляров оказывается трудным для реализации предложенной аннотации @Log поскольку это повлечет за собой использование фабрики для каждого отдельного экземпляра потенциально аннотированного класса. Очевидно, это добавило бы огромное количество стандартного кода. Вероятно, мы бы даже создали больше шаблонов, чем избегали, если бы не жестко закодировали инструкцию регистрации в методах. Кроме того, случайное использование конструктора внесло бы незначительные ошибки в Java-программу, потому что аннотации в таких экземплярах больше не обрабатывались бы так, как мы ожидаем. В качестве еще одной проблемы, фабрики нелегко компонуются. Что, если мы хотим добавить аннотацию @Log к классу, который уже является компонентом Hibernate? Это звучит тривиально, но для объединения обеих фреймворков потребуется обширная конфигурация. И, наконец, получившийся в результате фабричный код не получится слишком красивым для чтения, а переход на использование инфраструктуры будет дорогостоящим для реализации. Это где инструментарий с агентами Java вступает в действие. Эта недооцененная форма контрольно-измерительных приборов предлагает отличную альтернативу обсуждаемым контрольно-измерительным приборам подкласса.

Простой агент

Агент Java представлен простым файлом jar. Подобно обычным программам Java, агенты Java определяют некоторый класс как точку входа. Затем ожидается, что этот класс определит статический метод, который вызывается до вызова main метода фактической программы Java:

1
2
3
4
5
class MyAgent {
  public static void premain(String args, Instrumentation inst) {
    // implement agent here ...
  }
}

Наиболее интересной частью при работе с агентами Java является второй аргумент метода premain , который представляет экземпляр интерфейса Instrumentation . Этот интерфейс предлагает способ подключения к процессу загрузки классов Java путем определения ClassFileTransformer . С такими преобразователями мы можем улучшить любой класс Java-программы перед ее первым использованием.

Хотя использование этого API может показаться простым на первый взгляд, оно ставит новые задачи. Преобразования файлов классов выполняются путем изменения скомпилированных классов Java, которые представлены в виде байтового кода Java. На самом деле виртуальная машина Java не имеет представления о том, что такое язык программирования Java. Вместо этого он имеет дело только с этим байтовым кодом. И именно благодаря этой абстракции байт-кода JVM легко может работать с другими языками, такими как Scala или Groovy. Как следствие, преобразователь файла зарегистрированного класса предлагает только преобразовать данный байтовый (кодовый) массив в другой.

Хотя библиотеки, такие как ASM или BCEL, предлагают простой API для управления скомпилированными классами Java, лишь немногие разработчики имеют опыт работы с необработанным байтовым кодом. Что еще хуже, правильное манипулирование байтовым кодом часто бывает обременительным, и даже небольшие ошибки исправляются виртуальной машиной с использованием неприятного и неустранимого ошибки VerifierError . К счастью, есть лучшие, более простые способы манипулирования байтовым кодом.

Byte Buddy , библиотека, которую я написал и поддерживаю, предоставляет простой API как для манипулирования скомпилированными классами Java, так и для создания агентов Java. В некоторых аспектах Byte Buddy является библиотекой генерации кода, похожей на cglib и Javassist. Однако, кроме этих библиотек, Byte Buddy предлагает унифицированный API для реализации подклассов и для переопределения существующих классов. Однако в этой статье мы хотим лишь переопределить класс с помощью агента Java. Любопытные читатели обращаются к веб-странице Byte Buddy, которая предлагает подробное руководство по полному набору функций.

Использование Byte Buddy для простого агента

Byte Buddy предлагает один из способов определения инструментария — использование внедрения зависимостей. При этом класс-перехватчик, который представлен любым простым Java-объектом, просто запрашивает любую необходимую информацию посредством аннотаций к его параметрам. Например, используя аннотацию @Origin Байта Бадди для параметра типа « Method », Байт Бадди вычитает, что перехватчик хочет узнать о методе, который перехватывается. Таким образом, мы можем определить общий перехватчик, который всегда знает о методе, который перехватывается:

1
2
3
4
5
class LogInterceptor {
  static void log(@Origin Method method) {
    Logger.log(method + " was called");
  }
}

Конечно, Byte Buddy поставляется с большим количеством аннотаций.

Но как этот перехватчик представляет собой логику, которую мы намеревались предложить в рамках каркаса? Пока что мы определили только перехватчик, который регистрирует вызов метода. Что нам не хватает, так это последующий вызов исходного кода метода. К счастью, инструменты Byte Buddy являются составными. Сначала мы определяем MethodDelegation для недавно определенного LogInterceptor который по умолчанию вызывает статический метод перехватчика при каждом вызове метода. Исходя из этого, мы можем затем составить делегирование с последующим вызовом исходного кода метода, который представлен SuperMethodCall :

1
2
MethodDelegation.to(LogInterceptor.class)
  .andThen(SuperMethodCall.INSTANCE)

Наконец, нам нужно проинформировать Byte Buddy о методах, которые должны быть перехвачены указанным инструментарием. Как мы объясняли ранее, мы хотим, чтобы этот инструментарий применялся для любого метода, аннотируемого @Log . В Byte Buddy такое свойство метода может быть идентифицировано с помощью ElementMatcher который похож на предикат Java 8. В статическом служебном классе ElementMatchers мы уже можем найти подходящее средство сопоставления для идентификации методов с заданной аннотацией: ElementMatchers.isAnnotatedWith(Log.class) .

Со всем этим теперь мы можем определить агента, который реализует предложенную структуру ведения журнала. Для агентов Java Byte Buddy предоставляет служебный API, который основан на API модификации класса, который мы только что обсуждали. Подобно этому последнему API, он разработан как предметно-ориентированный язык, так что его значение должно быть легко понято только при взгляде на реализацию. Как мы видим, для определения такого агента требуется всего несколько строк кода:

01
02
03
04
05
06
07
08
09
10
11
class LogAgent {
  public static void premain(String args, Instrumentation inst) {
    new AgentBuilder.Default()
      .rebase(ElementMatchers.any())
      .transform( builder -> return builder
                              .method(ElementMatchers.isAnnotatedWith(Log.class))
                              .intercept(MethodDelegation.to(LogInterceptor.class)
                                  .andThen(SuperMethodCall.INSTANCE)) )
      .installOn(inst);
  }
}

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

Как насчет реальной жизни?

Конечно, представленный агент-регистратор является тривиальным примером. И часто широко используются фреймворки с широкой областью действия, которые предлагают схожие функции, такие как Spring или Dropwizard, например. Тем не менее, такие структуры одинаково часто высказывают мнение о том, как подходить к проблемам программирования. Для большого количества программных приложений это может не быть проблемой. И все же, иногда эти мнения мешают чему-то большему. Затем, работа вокруг предположения платформы о том, как что-либо делать, может вызвать не только несколько проблем, часто вызывает утечку абстракций и может просто привести к увеличению затрат на обслуживание программного обеспечения. Это особенно верно, когда приложения растут и изменяются со временем и расходятся по своим потребностям с тем, что предлагает базовая структура.

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

Я пришел к тому, что считаю, что по крайней мере любые сквозные проблемы должны решаться специализированной библиотекой, управляемой агентом, а не встроенным модулем монолитной структуры. И я действительно хотел бы, чтобы больше приложений рассмотрели этот подход. В самом тривиальном случае достаточно использовать агента, чтобы зарегистрировать слушателей на интересующие методы и получить его оттуда. Этот косвенный подход к созданию модулей кода позволяет избежать сильной сплоченности, которую я наблюдаю в большой части Java-приложений, с которыми я сталкиваюсь. Как хороший побочный эффект, он также делает тестирование очень простым. И подобно запуску тестов, без добавления агента при запуске приложения, позволяет демонстративно отключить определенную функцию приложения, такую ​​как, например, ведение журнала. Все это без изменения строки кода и без сбоя приложения, поскольку JVM просто игнорирует аннотации, которые не могут быть разрешены во время выполнения. Безопасность, ведение журналов, кеширование, есть много причин, по которым эти темы и многое другое должно решаться предлагаемым образом. Поэтому иногда делают агенты, а не фреймворки.

Ссылка: Создавайте агенты, а не фреймворки от нашего партнера JCG Рафаэля Винтерхальтера в блоге My daily Java .