Статьи

Практическое введение в внедрение кода с AspectJ, Javassist и Java Proxy

Способность вставлять фрагменты кода в скомпилированные классы и методы, статически или во время выполнения, может оказать огромную помощь. Это особенно относится к устранению неполадок в сторонних библиотеках без исходных кодов или в среде, где невозможно использовать отладчик или профилировщик. Внедрение кода также полезно для решения проблем, затрагивающих все приложение, таких как мониторинг производительности. Такое внедрение кода стало популярным под названием Aspect-Oriented Programming (AOP). Внедрение кода не то, что используется очень редко, как вы думаете, скорее наоборот; каждый программист попадет в ситуацию, когда эта способность может предотвратить большую боль и разочарование.

Эта статья направлена ​​на то, чтобы дать вам знания, которые могут вам понадобиться (или я бы лучше сказал «будет»), и убедить вас в том, что изучение основ внедрения кода действительно стоит потраченного времени. Я представлю три различных реальных случая, когда внедрение кода пришло мне на помощь, решая каждый из них с помощью отдельного инструмента, наилучшим образом подходящего под имеющиеся ограничения.

Зачем вам это нужно

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

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

  • Вы передаете коллекцию данных в библиотеку с закрытым исходным кодом для обработки, и один метод в библиотеке завершается сбоем для одного из элементов, но исключение не предоставляет никакой информации о том, каким элементом он был. Вам нужно изменить его, чтобы либо зарегистрировать ошибочный аргумент, либо включить его в исключение. (И вы не можете использовать отладчик, потому что это происходит только на рабочем сервере приложений.)
  • Вам необходимо собрать статистику производительности важных методов в вашем приложении, включая некоторые его компоненты с закрытым исходным кодом при типичной рабочей нагрузке. (В производстве вы, конечно, не можете использовать профилировщик, и вы хотите понести минимальные накладные расходы.)
  • Вы используете JDBC для отправки большого количества данных в базу данных в пакетном режиме, и одно из пакетных обновлений завершается неудачно. Вам понадобится хороший способ узнать, какая это партия и какие данные в ней содержатся.

Я действительно столкнулся с этими тремя случаями (среди прочих), и вы увидите возможные реализации позже.

Читая этот пост, вы должны помнить о следующих преимуществах внедрения кода:

  • Внедрение кода позволяет изменять двоичные классы, для которых у вас нет исходных кодов.
  • Внедренный код может использоваться для сбора различной информации времени выполнения в средах, где вы не можете использовать традиционные инструменты разработки, такие как профилировщики и отладчики.
  • Не повторяйте себя: если вам нужен один и тот же кусок логики в нескольких местах, вы можете определить его один раз и ввести его во все эти места.
  • С внедрением кода вы не изменяете исходные исходные файлы, поэтому он подходит для (возможно, крупномасштабных) изменений, которые вам нужны только в течение ограниченного периода времени, особенно с помощью инструментов, которые позволяют легко включать и отключать внедрение кода (например, AspectJ с ткачеством во время загрузки). Типичным случаем является сбор метрик производительности и увеличение регистрации во время устранения неполадок
  • Вы можете внедрить код либо статически, во время сборки, либо динамически, когда целевые классы загружаются JVM

Мини Глоссарий

Вы можете встретить следующие термины в отношении внедрения кода и AOP:

Совет

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

АОП (Аспектно-ориентированное программирование)

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

аспект

Единица модульности в AOP примерно соответствует классу — она ​​может содержать различные советы и указания.

Совместная точка

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

Pointcut

Грубо говоря, pointcut — это выражение, которое сообщает инструменту внедрения кода, куда вводить конкретный фрагмент кода, т.е. к каким точкам соединения применять конкретный совет. Он может выбрать только одну такую ​​точку — например, выполнение одного метода — или много похожих точек — например, выполнение всех методов, помеченных пользовательской аннотацией, такой как @MyBusinessMethod.

ткачество

Процесс внедрения кода — советы — в целевые места — точки соединения.


Инструменты

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

Основная классификация инструментов ввода кода

I. Уровень абстракции

Насколько сложно выразить вводимую логику и указать точки, в которые должна быть вставлена ​​логика?

Относительно кода «совета»:

  1. Прямое манипулирование байт-кодом (например, ASM) — чтобы использовать эти инструменты, вам необходимо понимать формат байт-кода класса, поскольку он очень мало абстрагируется от него, вы работаете непосредственно с кодами операций, стеком операндов и отдельными инструкциями. Пример ASM:

    methodVisitor.visitFieldInsn (Opcodes.GETSTATIC, «java / lang / System», «out», «Ljava / io / PrintStream;»);

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

  2. Промежуточный уровень — код в строках, некоторая абстракция структуры файла классов (Javassist)
  3. Советы в Java (например, AspectJ) — код для внедрения выражается в виде синтаксически проверенной и статически скомпилированной Java

Относительно спецификации, где вводить код:

  1. Ручная инъекция — вам нужно как-то ухватиться за то место, куда вы хотите внедрить код (ASM, Javassist)
  2. Примитивные pointcuts — у вас есть довольно ограниченные возможности для выражения, где вводить код, например, к определенному методу, ко всем открытым методам класса или ко всем открытым методам классов в группе (перехватчики Java EE)
  3. Выражения сопоставления точек с образцом — мощные выражения, сопоставляющие объединенные точки на основе ряда критериев с подстановочными знаками, осведомленность о контексте (например, «вызывается из класса в пакете XY») и т. Д. (AspectJ)

II. Когда случается магия

Код может быть введен в разные моменты времени:

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

Каждый из этих режимов впрыска может быть более подходящим в различных ситуациях.

III. Что это может сделать

Инструменты для внедрения кода сильно различаются в зависимости от того, что они могут или не могут делать. Вот некоторые из возможностей:

  • Добавить код до / после / вместо метода — только методы уровня члена или также статические?
  • Добавить поля в класс
  • Добавить новый метод
  • Сделать класс для реализации интерфейса
  • Изменить инструкцию в теле метода (например, вызов метода)
  • Модифицировать дженерики, аннотации, модификаторы доступа, изменять значения констант,…
  • Удалить метод, поле и т. Д.



Выбранные инструменты внедрения кода

Самые известные инструменты для внедрения кода:

  1. Динамический Java Proxy
  2. Библиотека манипулирования байт-кодом ASM
  3. JBoss Javassist
  4. AspectJ
  5. Spring AOP / прокси
  6. Перехватчики Java EE



Практическое введение в Java Proxy, Javassist и AspectJ

Я выбрал три довольно разных зрелых и популярных инструмента для внедрения кода и представлю их на реальных примерах, которые я лично испытал.

Вездесущий динамический Java-прокси

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

Преимущества:

  • Это часть JVM, поэтому она доступна везде
  • Вы можете использовать один и тот же прокси — точнее InvocationHandler — для несовместимых объектов и, таким образом, использовать код больше, чем обычно
  • Вы экономите усилия, потому что можете легко перенаправлять все вызовы целевому объекту и изменять только те, которые вам интересны. Если бы вы реализовали прокси вручную, вам нужно было бы реализовать все методы рассматриваемого интерфейса.

Недостатки:

  • Вы можете создать динамический прокси только для интерфейса, вы не можете использовать его, если ваш код ожидает конкретного класса
  • Вы должны создать экземпляр и применить его вручную, нет волшебной автоинъекции
  • Это слишком многословно
  • Его мощность очень ограничена, он может выполнять только некоторый код до / после / вокруг метода

Нет этапа ввода кода — вы должны применить прокси вручную.

пример

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

Важная часть кода:

LoggingStatementDecorator.java — фрагмент 1

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class LoggingStatementDecorator implements InvocationHandler {
 
   private PreparedStatement target;
   ...
 
   private LoggingStatementDecorator(PreparedStatement target) { this.target = target; }
 
   @Override
   public Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable {
 
      try {
         Object result = method.invoke(target, args);
         updateLog(method, args); // remember data, reset upon successful execution
         return result;
      } catch (InvocationTargetException e) {
         Throwable cause = e.getTargetException();
         tryLogFailure(cause);
         throw cause;
      }
 
   }
 
   private void tryLogFailure(Throwable cause) {
      if (cause instanceof BatchUpdateException) {
         int failedBatchNr = successfulBatchCounter + 1;
         Logger.getLogger("JavaProxy").warning(
               "THE INJECTED CODE SAYS: " +
               "Batch update failed for batch# " + failedBatchNr +
               " (counting from 1) with values: [" +
               getValuesAsCsv() + "]. Cause: " + cause.getMessage());
      }
   }
...

Заметки:

Чтобы создать прокси, вам сначала нужно реализовать InvocationHandler и его метод invoke, который вызывается всякий раз, когда на прокси вызывается любой из методов интерфейса.
Вы можете получить доступ к информации о вызове через объекты java.lang.reflect. * И, например, делегировать вызов проксируемому объекту через method.invoke.
У нас также есть служебный метод для создания экземпляра прокси для оператора Prepared:

LoggingStatementDecorator.java — фрагмент 2

1
2
3
4
5
6
public static PreparedStatement createProxy(PreparedStatement target) {
  return (PreparedStatement) Proxy.newProxyInstance(
      PreparedStatement.class.getClassLoader(),
      new Class[] { PreparedStatement.class },
      new LoggingStatementDecorator(target));
};

Заметки:

  • Вы можете видеть, что вызов newProxyInstance принимает загрузчик классов, массив интерфейсов, которые должен реализовывать прокси-сервер, и обработчик вызова, которому необходимо делегировать вызовы (сам обработчик должен управлять ссылкой на объект прокси, если он в этом нуждается)

Затем он используется так:

Main.java

1
2
3
4
5
6
...
PreparedStatement rawPrepStmt = connection.prepareStatement("...");
PreparedStatement loggingPrepStmt = LoggingStatementDecorator.createProxy(rawPrepStmt);
...
loggingPrepStmt.executeBatch();
...

Заметки:

  • Вы видите, что мы должны вручную обернуть необработанный объект с помощью прокси и использовать прокси далее



Альтернативные решения

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

Используя динамический Java-прокси, мы получаем довольно чистое и простое в реализации решение.

Независимый Javassist

JBoss Javassist — это инструмент для вставки промежуточного кода, обеспечивающий абстракцию более высокого уровня по сравнению с библиотеками манипулирования байт-кодом и предлагающий небольшие ограниченные, но все же очень полезные возможности манипуляции. Внедряемый код представляется в виде строк, и вам нужно вручную перейти к методу класса, где его внедрить. Его главное преимущество в том, что модифицированный код не имеет новых зависимостей времени выполнения, от Javassist или чего-либо еще. Это может стать решающим фактором, если вы работаете в большой корпорации, где развертывание дополнительных библиотек с открытым исходным кодом (или почти любых дополнительных библиотек), таких как AspectJ, затруднено по юридическим и другим причинам.

Преимущества:

  • Код, измененный Javassist, не требует каких-либо новых зависимостей во время выполнения, внедрение происходит во время сборки и сам внедренный код совета не зависит от какого-либо API Javassist
  • Более высокий уровень, чем у библиотек манипулирования байт-кодом, введенный код написан в синтаксисе Java, хотя и заключен в строки
  • Может делать большинство вещей, которые вам могут понадобиться, такие как «консультирование» вызовов методов и выполнение методов
  • Вы можете добиться как внедрения во время сборки (через код Java или пользовательскую задачу Ant для выполнения рекомендаций по выполнению / вызову ), так и внедрения во время загрузки (реализуя свой собственный агент Java 5+ [спасибо Антону])

Недостатки:

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

(См. GluonJ ниже для решения без большинства недостатков Javassist.)

С помощью Javassist вы создаете класс, который использует API Javassist для внедрения кода в целевые объекты int и запускает его как часть процесса сборки после компиляции, например, как я однажды делал с помощью пользовательской задачи Ant.

пример

Нам нужно было добавить простой мониторинг производительности в наше приложение Java EE, и нам не разрешили развернуть какую-либо неподтвержденную библиотеку с открытым исходным кодом (по крайней мере, без прохождения трудоемкого процесса утверждения). Поэтому мы использовали Javassist для внедрения кода мониторинга производительности в наши важные методы и туда, где были вызваны важные внешние методы.

Код инжектора:

JavassistInstrumenter.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class JavassistInstrumenter {
 
   public void insertTimingIntoMethod(String targetClass, String targetMethod) throws NotFoundException, CannotCompileException, IOException {
      Logger logger = Logger.getLogger("Javassist");
      final String targetFolder = "./target/javassist";
 
      try {
         final ClassPool pool = ClassPool.getDefault();
         // Tell Javassist where to look for classes - into our ClassLoader
         pool.appendClassPath(new LoaderClassPath(getClass().getClassLoader()));
         final CtClass compiledClass = pool.get(targetClass);
         final CtMethod method = compiledClass.getDeclaredMethod(targetMethod);
 
         // Add something to the beginning of the method:
         method.addLocalVariable("startMs", CtClass.longType);
         method.insertBefore("startMs = System.currentTimeMillis();");
         // And also to its very end:
         method.insertAfter("{final long endMs = System.currentTimeMillis();" +
            "iterate.jz2011.codeinjection.javassist.PerformanceMonitor.logPerformance(\"" +
            targetMethod + "\",(endMs-startMs));}");
 
         compiledClass.writeFile(targetFolder);
         // Enjoy the new $targetFolder/iterate/jz2011/codeinjection/javassist/TargetClass.class
 
         logger.info(targetClass + "." + targetMethod +
               " has been modified and saved under " + targetFolder);
      } catch (NotFoundException e) {
         logger.warning("Failed to find the target class to modify, " +
               targetClass + ", verify that it ClassPool has been configured to look " +
               "into the right location");
      }
   }
 
   public static void main(String[] args) throws Exception {
      final String defaultTargetClass = "iterate.jz2011.codeinjection.javassist.TargetClass";
      final String defaultTargetMethod = "myMethod";
      final boolean targetProvided = args.length == 2;
 
      new JavassistInstrumenter().insertTimingIntoMethod(
            targetProvided? args[0] : defaultTargetClass
            , targetProvided? args[1] : defaultTargetMethod
      );
   }
}

Заметки:

  • Вы можете увидеть «низкоуровневость» — вам нужно явно иметь дело с такими объектами, как CtClass, CtMethod, явно добавлять локальную переменную и т. Д.
  • Javassist достаточно гибок в том, что касается поиска классов для изменения — он может искать путь к классам, определенную папку, файл JAR или папку с файлами JAR
  • Вы должны скомпилировать этот класс и запустить его main во время процесса сборки

Javassist на стероидах: GluonJ

GluonJ — это инструментальный инструмент AOP, созданный на основе Javassist. Он может использовать либо собственный синтаксис, либо аннотации Java 5, и он построен на концепции «редакторов». Reviser — это класс, аспект, который пересматривает, то есть модифицирует конкретный целевой класс и переопределяет один или несколько его методов (в отличие от наследования, код редактора физически накладывается на исходный код внутри целевого класса).

Преимущества:

  • Отсутствие зависимостей времени выполнения, если используется ткачество во время сборки (для ткачества во время загрузки требуется библиотека агентов GluonJ или gluonj.jar)
  • Простой синтаксис Java с использованием аннотации GlutonJ — хотя пользовательский синтаксис также тривиален для понимания и прост в использовании
  • Простое автоматическое добавление к целевым классам с помощью инструмента JAR от GlutonJ, задачи Ant или динамически во время загрузки
  • Поддержка как во время сборки, так и во время загрузки

Недостатки:

  • Аспект может изменять только один класс, вы не можете внедрить один и тот же кусок кода в несколько классов / методов.
  • Ограниченная мощность — обеспечивает только добавление поля / метода и выполнение кода вместо целевого метода или вокруг него, либо при любом из его выполнений, либо только в том случае, если выполнение происходит в определенном контексте, т.е. при вызове из определенного класса / метода

Если вам не нужно внедрять один и тот же кусок кода в несколько методов, тогда GluonJ является более простым и лучшим выбором, чем Javassist, и если его простота не является проблемой для вас, то он также может быть лучшим выбором, чем AspectJ, просто благодаря этому простота.

Всемогущий аспект

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

Синтаксис советов AspectJ представлен в двух вариантах: один представляет собой расширенный набор синтаксиса Java с дополнительными ключевыми словами, такими как aspect и pointcut, другой — @AspectJ — является стандартным Java 5 с аннотациями, такими как @Aspect, @Pointcut, @Around. Последний, возможно, легче освоить и использовать, но он также не так эффективен, поскольку он не так выразителен, как пользовательский синтаксис AspectJ.

С AspectJ вы можете определить, какие совместные точки давать советы с помощью очень мощных выражений, но может быть немного трудно их изучить и правильно их понять. Существует полезный плагин Eclipse для разработки AspectJ — AspectJ Development Tools (AJDT) — но в прошлый раз, когда я попробовал его, он оказался не таким полезным, как мне бы хотелось.

Преимущества:

  • Очень мощный, может сделать почти все, что вам может понадобиться
  • Мощные выражения pointcut для определения, где вводить совет и когда его активировать (включая некоторые проверки во время выполнения) — полностью включает DRY, то есть писать один раз и вводить много раз
  • Инжекция кода во время сборки и загрузки (ткачество)

Недостатки:

  • Измененный код зависит от библиотеки времени выполнения AspectJ
  • Выражения pointcut очень мощные, но, возможно, их сложно понять правильно, и не существует большой поддержки их «отладки», хотя плагин AJDT частично способен визуализировать их эффекты.
  • Вероятно, потребуется некоторое время, чтобы начать работу, хотя базовое использование довольно простое (с использованием @Aspect, @Around и простого выражения pointcut, как мы увидим в примере)



пример

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

Код AspectJ довольно прост:

LoggingAspect.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Aspect
public class LoggingAspect {
 
   @Around("execution(private void TooQuiet3rdPartyClass.failingMethod(..))")
   public Object interceptAndLog(ProceedingJoinPoint invocation) throws Throwable {
      try {
         return invocation.proceed();
      } catch (Exception e) {
         Logger.getLogger("AspectJ").warning(
            "THE INJECTED CODE SAYS: the method " +
            invocation.getSignature().getName() + " failed for the input '" +
            invocation.getArgs()[0] + "'. Original exception: " + e);
         throw e;
      }
   }
}

Заметки:

  • Аспект представляет собой обычный Java-класс с аннотацией @Aspect, которая является просто маркером для AspectJ.
  • Аннотация @Around указывает AspectJ выполнить метод вместо метода, соответствующего выражению, то есть вместо failingMethod объекта TooQuiet3rdPartyClass
  • Метод round advice должен быть публичным, возвращать Object и принимать в качестве аргумента специальный объект AspectJ, несущий информацию о вызове — ProceedingJoinPoint, и он может иметь произвольное имя (на самом деле это минимальная форма подписи, она может быть более сложным.)
  • Мы используем ProceedingJoinPoint, чтобы делегировать вызов исходной цели (экземпляр TooQuiet3rdPartyClass) и, в случае исключения, получить значение аргумента
  • Я использовал совет @Around, хотя @AfterThrowing был бы проще и более уместным, но это лучше показывает возможности AspectJ и может быть приятно сопоставлено с примером динамического java-прокси выше

Поскольку я не контролировал среду приложения, я не мог включить переплетение времени загрузки и, следовательно, пришлось использовать задачу Ant AspectJ, чтобы сплетать код во время сборки, переупаковывать уязвимый JAR и повторно развертывать его в сервер.

Альтернативные решения

Ну, если вы не можете использовать отладчик, тогда ваши возможности весьма ограничены. Единственное альтернативное решение, о котором я мог подумать, — декомпилировать класс (недопустимо!), Добавить запись в метод (при условии, что декомпиляция прошла успешно), перекомпилировать его и заменить исходный .class на измененный.

Темная сторона

Внедрение кода и аспектно-ориентированное программирование являются очень мощными и иногда необходимыми как для устранения неполадок, так и в качестве обычной части архитектуры приложения, как мы можем видеть, например, в случае Java EE Enterprise Java Bean, где возникают проблемы бизнеса, такие как управление транзакциями и проверки безопасности. внедряется в POJO (хотя реализации на самом деле чаще используют прокси) или в Spring.

Однако есть цена, которую нужно заплатить с точки зрения возможного снижения понятности, так как поведение и структура во время выполнения отличаются от того, что вы ожидаете, основываясь на исходных кодах (если вы не знаете, проверять также источники аспектов или если не сделано внедрение явным образом с помощью аннотаций к целевым классам, таким как @Interceptors Java EE). Поэтому вы должны тщательно взвесить преимущества и недостатки внедрения кода / AOP — хотя при разумном использовании они не затеняют поток программы больше, чем интерфейсы, фабрики и т. Д. Аргумент о затенении кода, возможно, часто переоценивается .

Если вы хотите, чтобы пример АОП стал диким, проверьте исходные коды Glassbox , инструмента мониторинга производительности JavaEE (для этого вам может понадобиться карта, чтобы не потеряться слишком сильно).

Необычное использование инъекции кода и АОП

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

  • Типичные примеры: Caching & et al (например: на AOP в JBoss Cache ), управление транзакциями, ведение журнала, обеспечение безопасности, постоянство, безопасность потоков, восстановление после ошибок, автоматическая реализация методов (например, toString, equals, hashCode), удаленное взаимодействие
  • Реализация ролевого программирования (например, OT / J , с использованием BCEL) или архитектуры данных, контекста и взаимодействия
  • тестирование
    • Тестовое покрытие — введите код для записи, была ли выполнена строка во время тестового прогона или нет
    • Мутационное тестирование ( µJava , Jumble ) — внедрить «случайную» мутацию в приложение и убедиться, что тесты не пройдены
    • Pattern Testing — автоматическая проверка правильности реализации рекомендаций по архитектуре / дизайну / передовым методам в коде через AOP
    • Имитация аппаратных / внешних сбоев путем внедрения исключения
  • Помогите добиться нулевого оборота для приложений Java — JRebel использует AOP-подобный подход для плагинов инфраструктуры и интеграции с сервером, а именно его плагины используют Javassist для «бинарного исправления»
  • Решение проблем и недопущение кодирования обезьян с помощью шаблонов AOP, таких как создание рабочих объектов (превращение прямых вызовов в асинхронные с помощью Runnable и очереди ThreadPool / task) и Wormhole (предоставление контекстной информации от вызывающей стороны, доступной для вызываемого, без необходимости их передачи) через все слои как параметры и без ThreadLocal) — описано в книге AspectJ в действии
  • Работа с унаследованным кодом — переопределение класса, созданного при вызове конструктора (этот и аналогичный может использоваться для разрыва тесной связи с выполнимым объемом работы), обеспечение обратной совместимости o, обучение компонентов правильной реакции на изменения среды
  • Сохранение обратной совместимости API, не блокируя его способность развиваться, например, путем добавления обратно совместимых методов, когда возвращаемые типы были сужены / расширены ( Bridge Method Injector — использует ASM), или повторно добавляя старые методы и реализуя их в терминах новый API
  • Превращение POJO в бобы JMX



Резюме

Мы узнали, что внедрение кода может оказаться незаменимым для устранения неполадок, особенно при работе с библиотеками с закрытыми исходными кодами и сложными средами развертывания. Мы видели три довольно разных инструмента для внедрения кода — динамические Java-прокси, Javassist, AspectJ -, применяемые к реальным проблемам, и обсуждали их преимущества и недостатки, потому что разные инструменты могут подходить для разных случаев. Мы также упоминали, что внедрение кода / AOP не следует использовать чрезмерно, и рассмотрели некоторые примеры расширенных приложений внедрения кода / AOP.

Я надеюсь, что вы теперь понимаете, как внедрение кода может помочь вам, и знаете, как использовать эти три инструмента.

Исходные коды

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

1
2
3
4
5
6
git clone git://github.com/jakubholynet/JavaZone-Code-Injection.git
cd JavaZone-Code-Injection/
cat README
mvn -P javaproxy test
mvn -P javassist test
mvn -P aspectj   test

(Maven может занять несколько минут, чтобы загрузить его зависимости, плагины и фактические зависимости проекта.)

Дополнительные ресурсы

Подтверждения

Я хотел бы поблагодарить всех людей, которые помогли мне с этим постом и презентацией, включая мои колледжи, народ JRebel и соавтора GluonJ проф. Сигеру Чиба.

Ссылка: Практическое введение в внедрение кода с AspectJ, Javassist и Java Proxy от нашего партнера JCG Якуба Холи в блоге Holy Java .

Статьи по Теме: