У Java-машины есть один интересный параметр -javaagent
. О нем почему-то весьма мало сказано в документации по самой JVM, есть лишь описание в javadoc java.lang.instrument. Сам параметр появился начиная с Java 1.5, и позволяет получать доступ к механизму манипулирования с байт-кодом классов (трансформация, переопределение классов).
В командной строке он выглядит так:
-javaagent:jarpath[=options]
где jarpath
- путь к jar-нику содержащему класс agent-а, и options
- строка доп. параметров agent-а, передается при его вызове.
Сам параметер может содержаться несколько раз в строке параметров JVM, позволяя загружать несколько agent-ов. Указываемый JAR должен удовлетворять спецификации JAR-файла, т.е. иметь манифест файл META-INF/MANIFEST.MF, со следующими возможными атрибутами:
Premain-Class
(обязательный) - он содержит имя класса agent-а сpremain
методом.Boot-Class-Path
- содержит как абсолютные так и относительные пути поиска классов, для Bootstrap classloader-а. Относительный путь начинается от абсолютного пути самого JAR-файла. Пути могут указывать как на директорию, так и на jar-библиотеку. В качестве разделителя используются пробел. Эти пути будут использоваться в случае, если какие-то классы не были найдены стандартными механизмами Java.Can-Redefine-Classes
- указывает возможность переопределения классов, может иметь значениеtrue
, либоfalse
(по-умолчанию).Can-Retransform-Classes
(только в Java 1.6) - указывает возможность ретрансформации классов, может иметь значениеtrue
, либоfalse
(по-умолчанию).
Метод premain
, может иметь одну из следующих сигнатур:
public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
(только Java 1.6)
В agentArgs
передается сама строка options
, разработчик должен сам реализовать логику парсинга этой строки, если хочет передавать в ней несколько аргументов. Собственно сам интерфейс java.lang.instrument.Instrumentation
и предоставляет нам доступ к механизмам операций с байт-кодом. Сам метод premain
вызывается, как вы уже должно быть поняли, еще до выполнения метода main
приложения.
public class InstrumentExample {
private static Instrumentation inst;
public static void premain(String options, Instrumentation inst) {
inst.addTransformer(new SomeTransformer());
this.inst = inst;
}
public static void main(String args[] ) {
...
byte[] classBytes = ...
ClassDefinition classDef = new ClassDefinition(SomeClass.class, classBytes);
inst.redefineClasses(classDef);
...
}
public static class SomeTransformer implements ClassFileTransformer {
public byte[] transform(java.lang.ClassLoader loader,
java.lang.String className,
java.lang.Class classBeingRedefined,
java.security.ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException
{
...
}
}
...
}
Чтобы использовать механизм трансфорамации классов вам нужно реализовать интерфейс java.lang.instrument.ClassFileTransformer
. И зарегистрировать свою реализацию через метод Instrumentation.addTransformer
. Сам трасформер будет срабатывать каждый раз при:
- загрузке класса
ClassLoader.defineClass
- переопределении класса
Instrumentation.redefineClasses
- ретрансформации класса
Instrumentation.retransformClasses
.
Аргумент classfileBuffer
содержит байт-код текущей версии класса, и его нельзя модифицировать, для его переопределения трансформер должен вернуть новый массив байт, либо null
, если трансформация не была произведена. В случае, когда трансформеров несколько, то они будут вызваны по цепочке, при этом classfileBuffer
будет результатом предыдущего трансформера. Чтобы сообщить об ошибке при трансформации необходимо выбросить java.lang.instrument.IllegalClassFormatException
, при unchecked-ошибках результат будет такой, как если бы метод вернул null
.
Для переопределения класса необходимо указать выше описанный параметер в манифесте и использовать класс java.lang.instrument.ClassDefinition
. Только вот есть ограничения: переопределение класса не должно добавлять, удалять новые поля или методы, менять сигнатуру методов или иерархию наследования. Можно менять только тело методов, структура класса меняться не должна. Если в приложении уже используются экземпляры предыдущей версии класса, то с ними ничего не произойдет, однако при последующем создании класса, будет использована уже новая версия.
К сожалению, Java-платформа не предоставляет стандартных инструментов для модификации/генерации байт-кода. В этом случае можно воспользоваться уже готовыми фреймворками, например, Javaassist или BCEL.
Javaаgent реализован в фреймворке Spring, чтобы можно было использовать AOP для классов, вне контекста. Тестовый фреймворк Jmockit также реализует своего javaagent-а. Тем самым открывая возможность писать mock-и для классов, экземпляры которых вы не можете заменить обычными способами, либо они содержат статические методы, заменить которые возможно только до загрузки класса.
1 комментарий:
Спасибо за отличное объяснение
Отправить комментарий