Статьи

Внедрение инструментария для байт-кода времени сборки с Javassist

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

терминология

  • Инструментарий — добавление кода в существующие файлы .class
  • Ткачество — инструментарий физических файлов, т.е. применение советов к файлам классов
  • Совет — код, который «внедряется» в файл класса; обычно мы различаем рекомендации «до», «после» и «вокруг» в зависимости от того, как они применяются к методу
  • Pointcut — указывает, где применить совет (например, полное имя класса + имя метода или шаблон, который понимает инструмент AOP)
  • Инъекция — «логический» акт добавления кода в существующий класс с помощью внешнего инструмента
  • АОП — аспектно-ориентированное программирование

Javassist против AspectJ

Почему вы должны использовать Javassit поверх классического AOP-инструмента, такого как AspectJ? Ну, обычно это не так, потому что AspectJ проще в использовании, менее подвержен ошибкам и намного более мощный. Но бывают случаи, когда вы не можете его использовать, например, вам нужно изменить байт-код, но вы не можете позволить себе добавить какие-либо внешние зависимости. При выборе между ними учитывайте следующее:

Javassist:

  • Только базовые (но часто достаточные) инструментальные возможности
  • Только во время сборки — изменяет файлы .class
  • Измененный код не имеет дополнительных зависимостей (кроме тех, которые вы добавляете), т.е. вам не нужен javassist.jar во время выполнения
  • Прост в использовании, но не так прост, как AspectJ; код для ввода обрабатывается как строка, которая компилируется в байт-код Javassist

AspectJ:

  • Очень могущественный
  • Поддерживается как время сборки, так и время загрузки (когда класс загружается JVM), ткачество (инструментарий)
  • Измененный код зависит от библиотеки времени выполнения AspectJ (советы расширяют ее базовый класс, специальные объекты, используемые для обеспечения доступа к информации времени выполнения, такой как параметры метода)
  • Его использование ничем не отличается от обычного программирования на Java, особенно если вы используете синтаксис на основе аннотаций (@Pointcut, @Around и т. Д.). Рекомендации составляются перед использованием и, таким образом, проверяются компилятором.

Классическая библиотека манипулирования байт-кодом:

  • Слишком низкоуровневый, вам нужно определить и добавить инструкции байт-кода, в то время как Javassist позволяет вам добавлять фрагменты кода Java

Инструментарий с Javassist

О некоторых основных изменениях, которые вы можете сделать с Javassist. Это ни в коем случае не исчерпывающий список.

Объявление локальной переменной для передачи данных от до до после рекомендации

Если вам нужно передать некоторые данные из предварительного совета в последующий совет, вы не можете создать новую локальную переменную в коде, передаваемом Javassist (например, «int myVar = 5;»). Вместо этого вы должны объявить его с помощью CtMethod.addLocalVariable (имя строки, тип CtClass), а затем вы можете использовать его в коде, как до, так и после советы метода.

Пример:

final CtMethod method = ...;
method.addLocalVariable("startMs", CtClass.longType);
method.insertBefore("startMs = System.currentTimeMillis();");
method.insertAfter("{final long endMs = System.currentTimeMillis();" +
"System.out.println(\"Executed in ms: \" + (endMs-startMs));}");

Инструментарий выполнения метода

Добавление кода в самом начале или в самом конце метода:

// Advice my.example.TargetClass.myMethod(..) with a before and after advices
final ClassPool pool = ClassPool.getDefault();
final CtClass compiledClass = pool.get("my.example.TargetClass");
final CtMethod method = compiledClass.getDeclaredMethod("myMethod");

method.addLocalVariable("startMs", CtClass.longType);
method.insertBefore("startMs = System.currentTimeMillis();");
method.insertAfter("{final long endMs = System.currentTimeMillis();" +
"System.out.println(\"Executed in ms: \" + (endMs-startMs));}");

compiledClass.writeFile("/tmp/modifiedClassesFolder");
// Enjoy the new /tmp/modifiedClassesFolder/my/example/TargetClass.class

Существует также CtMethod.insertAfter (String code, boolean asFinally) — JavaDoc: если asFinally «true», то вставленный байт-код выполняется не только при нормальном возврате элемента управления, но и при возникновении исключения. Если этот параметр имеет значение true, вставленный код не может получить доступ к локальным переменным ».

Обратите внимание, что вы всегда передаете код в виде одного оператора, как в «System.out.println (\« Привет из введенного! \ »);» или как блок утверждений, заключенный в «{» и «}».

Инструментарий вызова метода

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

В следующем примере мы добавляем мониторинг производительности для всех вызовов javax.naming.NamingEnumeration.next ():

final CtClass compiledClass = pool.get("my.example.TargetClass");
final CtMethod[] targetMethods = compiledClass.getDeclaredMethods();
for (int i = 0; i < targetMethods.length; i++) {
targetMethods[i].instrument(new ExprEditor() {
public void edit(final MethodCall m) throws CannotCompileException {
if ("javax.naming.NamingEnumeration".equals(m.getClassName()) && "next".equals(m.getMethodName())) {
m.replace("{long startMs = System.currentTimeMillis(); " +
"$_ = $proceed($$); " +
"long endMs = System.currentTimeMillis();" +
"System.out.println(\"Executed in ms: \" + (endMs-startMs));}");
}
}
});
}

 

Вызов метода, представляющего интерес, заменяется другим кодом, который также выполняет исходный вызов с помощью специального оператора «$ _ = $ continue ($$);».

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

Проблема с инструментированием вызовов заключается в том, что вам необходимо знать все классы, которые (могут) включать их и, следовательно, должны быть обработаны. Не существует официального способа перечисления всех классов [возможно, соответствующих шаблону], которые видимы для JVM, хотя есть некоторые обходные пути (доступ к закрытому свойству Sun ClassLoader.classes). Таким образом, лучше всего — кроме перечисления их вручную — добавить папку или JAR с классами во внутренний путь к классам Javassist ClassPool (см. Ниже), а затем просканировать папку / JAR на наличие всех файлов .class, преобразовав их имена в имена классов. Что-то типа:

// Groovy code; the method instrumentCallsIn would perform the code above:
pool.appendClassPath("/path/to/a/folder");
new File("/path/to/a/folder").eachFileRecurse(FileType.FILES) {
 file -> instrumentCallsIn( pool.get(file.getAbsolutePath().replace("\.class$","").replace('/','.')) );}

Javassist and class-path configuration

You certainly wonder how does Javassist find the classes to modify. Javassist is actually extremely flexible in this regard. You obtain a class by calling

private final ClassPool pool = ClassPool.getDefault();
...
final CtClass targetClass = pool.get("target.class.ClassName");

The ClassPool can search a number of places, that are added to its internal class path via the simple call

/* ClassPath newCP = */ pool.appendClassPath("/path/to/a/folder/OR/jar/OR/(jarFolder/*)");

The supported class path sources are clear from the available implementations of  ClassPath: there is a ByteArrayClassPath, ClassClassPath, DirClassPath, JarClassPath, JarDirClassPath (used if the path ends with “/*”), LoaderClassPath, URLClassPath.

The important thing is that the class to be modified  or any class used in the code that you inject into it doesn’t need to be on the JVM classpath, it only needs to be on the pool’s class path.

Implementing mini-AOP with Javassist and Ant using a custom task

This part briefly describes how to instrument classes with Javassist via a custom Ant task, which can be easily integrated into a build process.

The corresponding part of the build.xml is:

<target name="declareCustomTasks" depends="compile">
<mkdir dir="${antbuild.dir}"/>

<!-- Javac classpath contains javassist.jar, ant.jar -->
<javac srcdir="${antsrc.dir}" destdir="${antbuild.dir}" encoding="${encoding}" source="1.4" classpathref="monitoringInjectorTask.classpath" debug="true" />

<taskdef name="javassistinject" classname="example.JavassistInjectTask"
classpathref="monitoringInjectorTask.classpath" loaderref="javassistinject"/>
<typedef name="call" classname="example.JavassistInjectTask$MethodDescriptor"
classpathref="monitoringInjectorTask.classpath" loaderref="javassistinject"/>
<typedef name="execution" classname="example.JavassistInjectTask$MethodDescriptor"
classpathref="monitoringInjectorTask.classpath" loaderref="javassistinject"/>
</target>

<target name="injectMonitoring" depends="compile,declareCustomTasks" description="Process the compiled classes and inject calls to the performance monitoring API to some of them (currently hardcoded in PerfmonAopInjector)">

<javassistinject outputFolder="${classes.dir}" logLevel="info">
<fileset dir="${classes.dir}" includes="**/*.class">
<!-- method executions to inject with performance monitoring -->
<execution name="someSlowMethod" type="my.MyClass" />
<!-- method calls to inject with performance monitoring -->
<call name="search" type="javax.naming.directory.InitialDirContext" metric="ldap" />
<call name="next" type="javax.naming.NamingEnumeration" metric="ldap" />
<call name="hasMore" type="javax.naming.NamingEnumeration" metric="ldap" />
</javassistinject>

</target>

 

Noteworthy:

  • I’ve implemented a simple custom Ant task with the class example.JavassistInjectTask, extending org.apache.tools.ant.Task. It has setters for attributes and nested elements and uses the custom class PerfmonAopInjector (not shown) to perform the actual instrumentation via Javassist API. Attributes/nested elements:
    • setLoglevel(EchoLevel level) – see the EchoTask
    • setOutputFolder(File out)
    • addConfiguredCall(MethodDescriptor call)
    • addConfiguredExecution(MethodDescriptor exec)
    • addFileset(FileSet fs) – use fs.getDirectoryScanner(super.getProject()).getIncludedFiles() to get the names of the files under the dir
  • MethodDescriptor is a POJO with a no-arg public constructor and setters for its attributes (name, type, metric), which is introduced to Ant via <typedef> and its instances are passed to the JavassistInjectTask by Ant using its addConfigured<name>, where the name equlas the element’s name, i.e. the name specified in the typedef
  • PerfmonAopInjector is another POJO that uses Javassist to inject execution time logging to method executions and calls as shown in the previous section, applying it to the classes/methods supplied by the JavassistInjectTask based on its <call .. /> and <execution … /> configuration
  • The fileset element is used both to tell Javassist in what directory it should look for classes and to find out the classes that may contain calls that should be instrumented (listing all the .class files and converting their names to class names)
  • All the typedefs use the same ClassLoader instance so that the classes can see each other, this is ensured by loaderref=»javassistinject» (its value is a custom identifier, same for all three)
  • The monitoringInjectorTask.classpath contains javassist.jar, ant.jar, JavassistInjectTask, PerfmonAopInjector and their helper classes
  • The classes.dir contains all the classes that may need to be instrumented and the classes used in the injected code, it’s added to the Javassist’s internal classpath via ClassPool.appendClassPath(“/absolute/apth/to/the/classes.dir”)

Notice that System.out|err.println called by any referenced class are automatically  intercepted by Ant and changed into Task.log(String msg, Project.MSG_INFO) and will be thus included in Ant’s output (unless -quiet).

PS: If using maven, you’ll be happy yo know that Javassist is in a Maven repository (well, at least it has a pom.xml, so I suppose so).

Ant custom task resources

  1. Rob Lybarger: Introduction to Custom Ant Tasks (2006) – the basics
  2. Rob Lybarger: More on Custom Ant Tasks (2006) – about nested elements
  3. Ant manual: Writing Your Own Task
  4. Stefan Bodewig: Ant 1.6 for Task Writers (2005)

 

From http://theholyjava.wordpress.com/2010/06/25/implementing-build-time-instrumentation-with-javassist/