Ответ 1
Короткий ответ
- Не перебирайте все загруженные классы из Instrumentation. Вместо этого просто просмотрите имя класса, переданное в трансформатор, и если оно соответствует вашему целевому классу, а затем преобразуйте его. В противном случае просто верните переданный classfileBuffer немодифицированный.
- Выполните настройки вызовов вне трансформатора (т.е. в вашем случае выполните следующие действия со своим агентом), чтобы инициализировать ваш трансформатор с именем класса, которое вы хотите преобразовать (это будет внутренний формат, поэтому вместо foo.bar.Snafu, вы увидите совпадение с foo/bar/Snafu. Затем добавьте трансформатор, переадресацию вызова и затем удалите трансформатор.
- Для вызова ретрансляции вам понадобится фактический класс [pre-transform], который вы можете найти, вызвав Class.forName(в agentmain), или если вам это абсолютно необходимо, вы можете найти его в Intrumentation.getAllLoadedClasses() в крайнем случае. Если целевой класс не был загружен, вам понадобится загрузчик классов для вызова Class.forName(name, boolean, classloader), и в этом случае вы можете передать URL-адрес целевому классу класса в основной строке arg агента.
Длинный ответ
Вот несколько рекомендаций:
- Отделите операцию, которую вы вызываете, на две отдельные операции:
- Установите агент. Это нужно сделать только один раз.
- Преобразование целевого класса [es]. Вы можете сделать это n раз.
- Я бы выполнил 1.2, зарегистрировав простой JMX MBean при установке агента. Этот MBean должен обеспечить такую операцию, как
public void transformClass(String className)
. и должен быть инициализирован ссылкой на экземпляр Instrumentation агента. Класс MBean, интерфейс и любые необходимые сторонние классы должны быть включены в ваш агент load.jar. Он также должен содержать ваш класс ModifyMethodTest (который, как я предполагаю, он уже делает). - В то же время, когда вы устанавливаете баннер агента, установите агента управления из $JAVA_HOME/lib/management-agent.jar, который активирует агент управления, чтобы вы могли вызвать операцию преобразования в MBean, который вы собираетесь зарегистрировать.
- Параметрируйте класс DemoTransformer, чтобы принять внутреннюю форму имени класса, который вы хотите преобразовать. (т.е. если ваше двоичное имя класса является foo.bar.Snafu, внутренняя форма будет foo/bar/Snafu. Поскольку ваш экземпляр DemoTransformer начинает получать обратные вызовы преобразования, игнорируйте все имена классов, которые не соответствуют указанному внутреннему имени класса формы. (т.е. просто вернуть classfileBuffer немодифицированный)
- Транспондер MBean transformClass должен затем:
- Преобразовать переданное имя класса во внутреннюю форму.
- Создайте новый DemoTransformer, передав имя внутреннего класса формы.
- Зарегистрируйте экземпляр DemoTransformer с помощью
Instrumentation.addTransformer(theNewDemoTransformer, true)
. - Вызов
Instrumentation.retransformClasses(ClassForName(className))
(с именем двоичного класса, переданным в операцию MBean). Когда этот вызов будет возвращен, ваш класс будет преобразован. - Извлеките трансформатор с помощью
Intrumentation.removeTransformer(theNewDemoTransformer)
.
Ниже приведена непроверенная аппроксимация того, что я имею в виду:
Трансформатор MBean
public interface TransformerServiceMBean {
/**
* Transforms the target class name
* @param className The binary name of the target class
*/
public void transformClass(String className);
}
Трансформаторная служба
public class TransformerService implements TransformerServiceMBean {
/** The JVM instrumentation instance */
protected final Instrumentation instrumentation;
/**
* Creates a new TransformerService
* @param instrumentation The JVM instrumentation instance
*/
public TransformerService(Instrumentation instrumentation) {
this.instrumentation = instrumentation;
}
/**
* {@inheritDoc}
* @see com.heliosapm.shorthandexamples.TransformerServiceMBean#transformClass(java.lang.String)
*/
@Override
public void transformClass(String className) {
Class<?> targetClazz = null;
ClassLoader targetClassLoader = null;
// first see if we can locate the class through normal means
try {
targetClazz = Class.forName(className);
targetClassLoader = targetClazz.getClassLoader();
transform(targetClazz, targetClassLoader);
return;
} catch (Exception ex) { /* Nope */ }
// now try the hard/slow way
for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
if(clazz.getName().equals(className)) {
targetClazz = clazz;
targetClassLoader = targetClazz.getClassLoader();
transform(targetClazz, targetClassLoader);
return;
}
}
throw new RuntimeException("Failed to locate class [" + className + "]");
}
/**
* Registers a transformer and executes the transform
* @param clazz The class to transform
* @param classLoader The classloader the class was loaded from
*/
protected void transform(Class<?> clazz, ClassLoader classLoader) {
DemoTransformer dt = new DemoTransformer(clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true);
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException("Failed to transform [" + clazz.getName() + "]", ex);
} finally {
instrumentation.removeTransformer(dt);
}
}
}
Трансформатор класса
public class DemoTransformer implements ClassFileTransformer {
/** The internal form class name of the class to transform */
protected String className;
/** The class loader of the class */
protected ClassLoader classLoader;
/**
* Creates a new DemoTransformer
* @param className The binary class name of the class to transform
* @param classLoader The class loader of the class
*/
public DemoTransformer(String className, ClassLoader classLoader) {
this.className = className.replace('.', '/');
this.classLoader = classLoader;
}
/**
* {@inheritDoc}
* @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[])
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals(this.className) && loader.equals(classLoader)) {
return new ModifyMethodTest(classfileBuffer).modiySleepMethod();
}
return classfileBuffer;
}
}
Агент
public class AgentMain {
public static void agentmain (String agentArgs, Instrumentation inst) throws Exception {
TransformerService ts = new TransformerService(inst);
ObjectName on = new ObjectName("transformer:service=DemoTransformer");
// Could be a different MBeanServer. If so, pass a JMX Default Domain Name in agentArgs
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(ts, on);
// Set this property so the installer knows we're already here
System.setProperty("demo.agent.installed", "true");
}
}
Установщик агентов
public class AgentInstaller {
/**
* Installs the loader agent on the target JVM identified in <code>args[0]</code>
* and then transforms all the classes identified in <code>args[1..n]</code>.
* @param args The target JVM pid in [0] followed by the classnames to transform
*/
public static void main(String[] args) {
String agentPath = "D:\\work\\workspace\\myjar\\loaded.jar";
String vid = args[0];
VirtualMachine vm = VirtualMachine.attach(vid);
// Check to see if transformer agent is installed
if(!vm.getSystemProperties().contains("demo.agent.installed")) {
vm.loadAgent(agentPath);
// that property will be set now,
// and the transformer MBean will be installed
}
// Check to see if connector is installed
String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null);
if(connectorAddress==null) {
// It not, so install the management agent
String javaHome = vm.getSystemProperties().getProperty("java.home");
File managementAgentJarFile = new File(javaHome + File.separator + "lib" + File.separator + "management-agent.jar");
vm.loadAgent(managementAgentJarFile.getAbsolutePath());
connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null);
// Now it installed
}
// Now connect and transform the classnames provided in the remaining args.
JMXConnector connector = null;
try {
// This is the ObjectName of the MBean registered when loaded.jar was installed.
ObjectName on = new ObjectName("transformer:service=DemoTransformer");
// Here we're connecting to the target JVM through the management agent
connector = JMXConnectorFactory.connect(new JMXServiceURL(connectorAddress));
MBeanServerConnection server = connector.getMBeanServerConnection();
for(int i = 1; i < args.length; i++) {
String className = args[i];
// Call transformClass on the transformer MBean
server.invoke(on, "transformClass", new Object[]{className}, new String[]{String.class.getName()});
}
} catch (Exception ex) {
ex.printStackTrace(System.err);
} finally {
if(connector!=null) try { connector.close(); } catch (Exception e) {}
}
// Done. (Hopefully)
}
}
================= UPDATE =========================
Эй, Ник; Да, это одно из ограничений существующих (т.е. Трансформаторов класса Java 5-8). Процитировать Инструментарий javadoc:
"Ретрансформация может изменять тела методов, постоянный пул и атрибутов. Ретрансформация не должна добавлять, удалять или переименовывать поля или методы, изменить подписи методов или изменить наследование. Эти ограничения могут быть отменены в будущих версиях. Файл класса байты не проверяются, проверяются и не устанавливаются до тех пор, пока были применены преобразования, если результирующие байты ошибочны этот метод генерирует исключение".
Как и в стороне, это же ограничение документируется дословно для переопределения классов.
Таким образом, у вас есть 2 варианта:
-
Не добавляйте новые методы. Это обычно очень ограничивает и дисквалифицирует использование очень распространенных шаблонов AOP байтового кода, таких как метод оберточная бумага. В зависимости от используемой вами библиотеки манипулирования байтов вы можете вставлять все необходимые функции в существующие методы. Некоторые библиотеки делают это проще других. Или, я бы сказал, некоторые библиотеки сделают это легче, чем другие.
-
Преобразуйте класс перед загрузкой класса. Это использует тот же общий шаблон кода, который мы уже обсуждали, за исключением того, что вы не запускаете преобразование через вызовы reansformClasses. Скорее, вы регистрируете ClassFileTransformer для выполнения преобразования до загрузки класса и ваш целевой класс будет изменен при загрузке первого класса. В этом случае вы в значительной степени можете модифицировать класс любым способом, например, при условии, что конечный продукт все еще может быть проверен. Избиение приложения на удар (т.е. Получение вашего ClassFileTransformer зарегистрированный до загрузки приложения классом), скорее всего, потребуется команда типа javaagent, хотя, если у вас жесткий контроль жизненного цикла вашего приложения, это можно сделать в более традиционном коде уровня приложения. Как я уже сказал, вам просто нужно сделать убедитесь, что вы получили трансформатор, зарегистрированный до загрузки целевого класса.
Еще одна вариация №2, которую вы можете использовать, - это имитировать новый класс с помощью нового загрузчика классов. Если вы создаете новый изолированный classloader, который не будет делегировать существующий [загруженный] класс, но имеет доступ к [unloaded] байт-кода целевого класса, вы по существу воспроизводите требования № 2 выше, поскольку JVM считает это совершенно новым классом.
================ ОБНОВЛЕНИЕ =================
В ваших последних комментариях, я чувствую, что немного потерял информацию о вашем местонахождении. Во всяком случае, Oracle JDK 1.6 наиболее определенно поддерживает ретрансляцию. Я не очень хорошо знаком с ASM, но последняя ошибка, которую вы указали, указывает на то, что преобразование ASM каким-то образом изменило схему класса, которая не разрешена, поэтому сбой повторной передачи.
Я понял, что рабочий пример добавит большей ясности. Те же классы, что и выше (плюс один тестовый класс под названием Person), здесь. Там есть несколько модификаций/дополнений:
- Операция преобразования в TransformerService теперь имеет 3 параметра:
- Имя двоичного класса
- Имя метода для инструмента
- [регулярное] выражение для соответствия сигнатуре метода. (если нулевой или пустой, соответствует всем подписям)
- Фактическая модификация байт-кода выполняется с помощью Javassist в классе ModifyMethodTest. Все инструменты - это добавить System.out.println, который выглядит так:
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
- AgentInstaller (у которого есть демонстрационный файл Main), просто самостоятельно устанавливает агент и службу преобразования. (Проще для Dev/Demo, но все равно будет работать с другими JVM).
- После того, как агент будет установлен самостоятельно, основной поток создает экземпляр Person и просто петли, вызывая методы Person Two sayHello.
До преобразования этот вывод выглядит следующим образом.
Temp File:c:\temp\com.heliosapm.shorthandexamples.AgentMain8724970986698386534.jar
Installing AgentMain...
AgentMain Installed
Agent Loaded
Instrumentation Deployed:true
Hello [0]
Hello [0]
Hello [1]
Hello [-1]
Hello [2]
Hello [-2]
У человека есть 2 метода sayHello, один принимает int, другой принимает String. (Строка только печатает отрицательный индекс цикла).
Как только я запустил AgentInstaller, агент установлен, и Person вызывается в цикле, я подключаюсь к JVM с помощью JConsole:
Я перехожу к MBean TransformerService и вызываю операцию transformClass. Я предоставляю полное имя класса [binary], имя метода для инструмента и выражение регулярного выражения (I) V, которое соответствует только методу sayHello, который принимает int как аргумент. (Или я мог бы поставить . *, или ничего, чтобы соответствовать всем перегрузкам). Я выполняю операцию.
Теперь, когда я возвращаюсь к запущенной JVM и просматриваю вывод:
Examining class [com/heliosapm/shorthandexamples/Person]
Instrumenting class [com/heliosapm/shorthandexamples/Person]
[ModifyMethodTest] Adding [System.out.println("\n\t-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]");]
[ModifyMethodTest] Intrumented [1] methods
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
Hello [108]
Hello [-108]
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
Hello [109]
Hello [-109]
Готово. Метод с инструментами.
Имейте в виду, причина, по которой разрешена ретрансляция, заключается в том, что модификация байт-кода Javassist не вносила никаких изменений, кроме как вводить код в существующий метод.
Имеют смысл?