В этой статье мы рассмотрим пакет java.lang.instrument.Instrumentation, с помощью которого можно поменять байт-код класса на этапе его загрузки.
Эта статья из цикла обработки аннотаций, обработчик аннотаций с RetentionPolicy.SOURCE мы уже написали. Теперь обработаем аннотации с RetentionPolicy.CLASS, и это уже совсем другая история.
Этапы компиляции исходников, загрузки классов
Как известно, сначала исходные Java-файлы компилируются в файлы с расширением .class. В предыдущей статье мы рассматривали, как сгенерировать дополнительный класс с исходным кодом на этапе компиляции (опираясь на существующий исходный код с аннотациями).
Далее, после запуска приложения .class файлы загружаются в память с помощью Classloader-ов, и для каждого класса создается объект типа java.lang.Class. Этот объект содержит описание класса: его методы, поля, аннотации и т.д. Так вот еще до загрузки класса и создания объектов Class тоже можно вмешаться и поменять байт-код.
Агент
Эту возможность дает библиотека инструментирования. С ее помощью можно написать служебное приложение, или так называемый агент.
Это инструмент, который может мониторить и изменять основное приложение. Например, измерять память, занимаемую объектами в процессе работы приложения. Или вмешаться и до загрузки классов поменять байт-код.
В случае модификации байт-кода агент служит посредником-модификатором.
Чтобы агент вмешался в процесс, его обычно собирают как отдельное приложение и передают как параметр VM при запуске основного приложения.
Агент берет откомпилированные классы, меняет их и пропускает на этап загрузки измененными. Классы загружаются в память уже измененными, и уже на основе измененного байт-кода в основном приложении создаются объекты типа java.lang.Class.
Создание агента
Сначала перечислим формальные особенности приложения-агента. Во-первых, это его главный метод.
Метод premain()
Агент отличается специфическим методом premain() вместо main(). Название метода premain() говорящее — агент запускается до основного приложения.
Так же как main() — стартовая точка приложения, premain() — стартовая точка агента.
Обратите внимание, что вторым параметром premain() является Instrumentation из пакета инструментирования:
import java.lang.instrument.Instrumentation;
public class AgentMain {
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("Classes loaded: " + instrumentation.getAllLoadedClasses().length);
instrumentation.addTransformer(new ClassTransformer());
}
}
Здесь мы выводим все загруженные классы и добавляем класс, который будет трансформировать байт код. Его мы подробнее рассмотрим ниже.
Создание манифеста
Вторая особенность: так же как для обычного приложения в манифесте указывают класс с методом main(), для агента указывают класс с методом premain().
Итоговый jar-файл должен содержать файл манифеста META-INF/MANIFEST.MF, и в нем должна быть строка:
Premain-Class: ru.sysout.bytecode.AgentMain
Чтобы не создавать манифест вручную, можно создавать манифест с помощью Maven.
Например, с помощью плагина maven-assembly-plugin это делается так:
<build>
<finalName>agent</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<index>true</index>
<manifestEntries>
<Premain-Class>ru.sysout.bytecode.AgentMain</Premain-Class>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Теперь перейдем к задаче — что же мы будем трансформировать.
Задача
Сразу скажу, что основное приложение у нас простейшее — один Main класс с одним пустым методом m(). В main() он вызывается. Код приложения приведен чуть ниже в разделе об использовании аннотации — метод m() аннотирован.
Трансформировать байт-код мы будем так — изменим код помеченных аннотацией @Log методов, а именно: в начало метода добавим вывод в консоль сообщения. Так вызовы методов будут логироваться в консоль. В нашем случае будет логироваться вызов единственного метода m().
Трансформировать байт-код аннотированных методов будем с помощью ClassTransformer.
Создание аннотации
Для начала создадим аннотацию в приложении-агенте:
public @interface Log {
String message();
}
Важно также:
ClassTransformer
Теперь главное — напишем класс, который меняет байт-код.
Для трансформации нам понадобится библиотека javaassist:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.24.0-GA</version>
</dependency>
Последнюю версию можно найти тут.
Наш класс должен реализовывать интерфейс ClassFileTransformer из библиотеки javaassist:
public class ClassTransformer implements ClassFileTransformer {
private ClassPool pool= ClassPool.getDefault();
public byte[] transform(ClassLoader loader,
String className,
Class classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try {
pool.insertClassPath(new ByteArrayClassPath(className, classfileBuffer));
CtClass cclass = pool.get(className.replaceAll("/", "."));
if (!cclass.getName().startsWith("ru.sysout.")) {
return null;
}
for (CtMethod currentMethod : cclass.getDeclaredMethods()) {
Log annotation = (Log) currentMethod.getAnnotation(Log.class);
if (annotation != null) {
currentMethod.insertBefore("{System.out.println(\"" + annotation.message() + "\");}");
}
}
return cclass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Этот метод возвращает новый байт-код класса либо null, если ничего не поменялось. Вызывается он для каждого класса приложения.
В методе мы:
- Проверяем, что класс находится в нашем пакете.
- Перебираем в цикле методы класса.
- Если нашли аннотированный метод, то изменяем его, вставляя в самое начало сообщение из аннотации.
Использование аннотации в основном приложении
Как уже было сказано, чтобы использовать аннотацию, надо включить агент как Maven-зависимость основного приложения:
<dependencies>
<dependency>
<groupId>ru.sysout</groupId>
<artifactId>bytecodetransform-agent</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
Теперь можно создать и аннотировать метод method() в нашем основном приложении. Мы вызываем его один раз:
public class Main {
public static void main(String... args) {
new Main().method();
}
@Log(message = "method called")
private void method() {
}
}
Этот метод ничего не делает. Но после трансформации байт кода он будет выводить в консоль значение message аннотации @Log:
method called
Чтобы вывод сработал, надо запустить наше основное приложение с агентом.
Как запускать основное приложение с агентом
Для этого надо передать агент agent.jar в параметре -javaagent виртуальной машины. То есть если наше основное приложение собирается в main.jar, то запускать его надо так:
java -jar main.jar -javaagent:"path-to\agent.jar"
Кавычки (на Windows) двойные.
Как результат в консоли мы получим:
Classes loaded: 732 method called
Можно задать параметры VM и в сборке Maven, но это оставим на отдельный коммент, если кому-то понадобится.
Можно не собирать основной main.jar, а запустить Main класс прямо из IDE:

В любом случае надо задать в параметрах VM путь до агента.
Итоги
Код доступен на GitHub.