Как я могу изменить класс java.lang на лету?

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

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

PS: Пожалуйста, сохраните меня "корень всех злых слов"

Уточнение варианта использования:

Представьте, что у меня есть класс с ThreadLocal:


class A {
   ThreadLocal<Counter> counter;
   ...
   counter.get().inc()
}

Я хотел бы заменить это аннотацией:


class A {
   @ThreadLocal
   Counter counter;
   ...
   counter.inc()
}

Но вместо того, чтобы сгенерировать приведенный выше код, я хотел бы изменить Thread так, чтобы Thread теперь имел поле Acounter, а фактический код был бы таким:


class A {
   // Nothing here, field is now in Thread
   ...
   Thread.currentThread().Acounter.inc()
}

person Nitsan Wakart    schedule 07.06.2013    source источник
comment
не могли бы вы добавить то, что у вас есть или над чем вы работали в настоящее время ... код, чтобы мы могли лучше помочь вам, пожалуйста?   -  person grepit    schedule 07.06.2013
comment
Извините, это находится на этапе предварительного исследования, в данный момент код недоступен. Я отредактирую вопрос, чтобы прояснить намерение.   -  person Nitsan Wakart    schedule 07.06.2013


Ответы (8)


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

Это ограничение может быть снято в рамках JEP-159, но, как обсуждалось на почтовая группа, это очень важное изменение, поэтому оно может вообще никогда не произойти.

Использование Javaassist/аналогичного позволит преобразовать класс в новый класс с новыми методами/полями. Этот класс можно загрузить с помощью ClassLoader и использовать во время выполнения, но его определение не заменит существующие экземпляры. Таким образом, будет невозможно использовать этот метод в сочетании с агентом для переопределения класса, поскольку переопределение инструментария ограничено следующим образом: «Переопределение может изменить тела методов, постоянный пул и атрибуты. Переопределение не должно добавлять, удалять или переименовывать поля..." см. здесь.

Так что пока НЕТ.

person Nitsan Wakart    schedule 18.06.2013

Если вы хотите изменить поведение «класса» во время выполнения, вы можете попробовать javasist. API здесь

person Chris    schedule 07.06.2013
comment
Я ищу способ изменить классы, которые уже загружены и выделены. Я могу найти достаточно документации, чтобы поддержать изменение класса по мере его загрузки, но не как заменить работающий поток моей новой версией. - person Nitsan Wakart; 07.06.2013

Я видел собственное решение для загрузки классов, которое динамически перезагружало JAR-файлы - вы определяете один ClassLoader для каждого JAR-файла и используете его для загрузки классов из этого JAR-файла; чтобы перезагрузить весь JAR, вы просто «убиваете» его экземпляр ClassLoader и создаете другой (после замены файла JAR).

Я не думаю, что таким образом можно настроить внутренний класс Java Thread, потому что у вас нет контроля над System ClassLoader. Возможное решение состоит в том, чтобы иметь класс CustomThreadWeaver, который будет генерировать новый класс, расширяющий Thread, с нужными вам переменными и использовать пользовательский DynamicWeavedThreadClassLoader для их загрузки.

Удачи и покажи нам своего монстра, когда у тебя все получится ;-)

person Cebence    schedule 12.06.2013

Возможно с использованием инструментария и, возможно, библиотек, таких как javassist, для изменения кода на лету. (однако добавление и удаление полей, методов или конструкторов в настоящее время невозможно)

//Modify code using javassist and call CtClass#toBytecode() or load bytecode from file
byte[] nevcode;
Class<?> clz = Class.forName("any.class.Example");
instrumentationInstace.redefineClasses(new ClassDefinition(clz, nevcode));

Не забудьте добавить Can-Redefine-Classes: true в манифест вашего java-агента.

Реальный пример — оптимизация java ‹ 9 string.replace(CharSequence, CharSequence) с помощью javassist:

String replace_src = 
    "{String str_obj = this;\n"
    + "char[] str = this.value;\n"
    + "String find_obj = $1.toString();\n"
    + "char[] find = find_obj.value;\n"
    + "String repl_obj = $2.toString();\n"
    + "char[] repl = repl_obj.value;\n"
    + "\n"
    + "if(str.length == 0 || find.length == 0 || find.length > str.length) {\n"
    + "    return str_obj;\n"
    + "}\n"
    + "int start = 0;\n"
    + "int end = str_obj.indexOf(find_obj, start);\n"
    + "if(end == -1) {\n"
    + "    return str_obj;\n"
    + "}\n"
    + "int inc = repl.length - find.length;\n"
    + "int inc2 = str.length / find.length / 512;\ninc2 = ((inc2 < 16) ? 16 : inc);\n"
    + "int sb_len = str.length + ((inc < 0) ? 0 : (inc * inc2));\n"
    + "StringBuilder sb = (sb_len < 0) ? new StringBuilder(str.length) : new StringBuilder(sb_len);\n"
    + "while(end != -1) {\n"
    + "    sb.append(str, start, end - start);\n"
    + "    sb.append(repl);\n"
    + "    start = end + find.length;\n"
    + "    end = str_obj.indexOf(find_obj, start);\n"
    + "}\n"
    + "if(start != str.length) {\n"
    + "    sb.append(str, start, str.length - start);\n"
    + "}\n"
    + "return sb.toString();\n"
    +"}";


ClassPool cp = new ClassPool(true);
CtClass clz = cp.get("java.lang.String");
CtClass charseq = cp.get("java.lang.CharSequence");

clz.getDeclaredMethod("replace", new CtClass[] {
        charseq, charseq
}).setBody(replace_src);

instrumentationInstance.redefineClasses(new ClassDefinition(Class.forName(clz.getName(), false, null), clz.toBytecode()));
person Possible    schedule 14.10.2015

Кажется, это вопрос использования правильного инструмента для работы. Аналогичный вопрос был задан здесь: Еще один вопрос о переполнении стека и библиотека манипулирования байт-кодом Javaassist была возможным решением.

Но без дальнейших подробностей о причинах, по которым это делается, кажется, что реальный ответ — использовать правильный инструмент для работы. Например, Groovy позволяет динамически добавить методы в язык.

person droozen    schedule 17.06.2013
comment
Добавление нового метода не похоже на добавление нового поля, так что это не помогает. Если вы знаете, как добавить новое поле на лету с помощью Javaassist, расскажите, пожалуйста. Мотивация — это интеллектуальное упражнение, мысль/идея на данный момент. - person Nitsan Wakart; 18.06.2013
comment
Ах я вижу. Я сам не использовал Javaassist, но нашел этот учебник, в котором утверждается, что он может добавить поле: csg.is.titech.ac.jp/~chiba/javassist/tutorial/ - person droozen; 18.06.2013

Вы можете попробовать создать агент JVM, который использует java.lang.instrument API и, более конкретно, использовать метод повторного преобразования, который "облегчает инструментирование уже загруженных классов" и затем используйте Javassist (или ASM), как уже упоминалось, для работы с байт-кодом.

Дополнительная информация об API java.lang.instrument

person Rafael Oltra    schedule 18.06.2013
comment
К сожалению, документы утверждают, что это невозможно: переопределение может изменить тела методов, постоянный пул и атрибуты. Переопределение не должно добавлять, удалять или переименовывать поля ... см. здесь - person Nitsan Wakart; 18.06.2013

Чтобы сделать то, что вы хотите, более простой альтернативой было бы использовать подкласс Thread, запустить его, а затем внутри этого потока выполнить код из вашего примера (вместе с приведением currentThread() к вашему подклассу).

person kutschkem    schedule 18.06.2013
comment
обратите внимание, что этот подход также должен работать с динамически генерируемыми подклассами, созданными, например, с помощью джавасист. - person kutschkem; 18.06.2013
comment
Это не то, чего я хочу... Я хочу добавлять поля на лету. Не делать это на лету, как вы описываете. - person Nitsan Wakart; 19.06.2013
comment
Но для того, что вы описываете, вы должны использовать динамические подклассы imho. Что делать, если у вас есть несколько потоков, где один использует A, а второй использует B. Вы действительно хотите создать поле в одном и том же классе Thread для обоих? Кроме того, проблема, которую я вижу, заключается в том, что ThreadLocals являются членами экземпляра. Поэтому вы должны создать один объект для экземпляра A, а не одно поле для класса A (хорошо, я предполагаю, что пример был только для демонстрации проблемы) - person kutschkem; 19.06.2013
comment
ну неважно, видя, что ThreadLocal должен присутствовать во ВСЕХ потоках, я думаю, вы правы, что вам нужно каким-то образом переправить свое поле в класс Thread. - person kutschkem; 19.06.2013

То, что вы пытаетесь сделать, невозможно.

Поскольку вы уже знаете о ThreadLocal, вы уже знаете, какое предлагаемое решение.

В качестве альтернативы вы можете создать подкласс Thread и добавить свои собственные поля; однако только те потоки, которые вы явно создаете для этого класса, будут иметь эти поля, поэтому вам все равно придется «возвратиться» к использованию локального потока.

Настоящим вопросом является «почему?», например, «почему локальный поток недостаточен для ваших требований?»

person cpurdy    schedule 13.06.2013
comment
Это твой настоящий вопрос, а не мой :). Измерьте разницу в производительности между наличием поля в Thread и использованием ThreadLocal, вот почему. Почему это невозможно? Существуют продукты, такие как JRebel, которые утверждают, что могут выполнять горячую замену вашего кода во время выполнения, так почему же горячая замена Thread невозможна? - person Nitsan Wakart; 14.06.2013
comment
Но что было неверным в ответе? Кстати, JRebel работает, изменяя классы во время загрузки, добавляя дополнительное поле к любому классу, который необходимо модифицировать. Вы можете сделать это самостоятельно, изменив Thread.class в rt.jar, поскольку вам не нравятся разумные предложения. - person cpurdy; 09.08.2017