Может ли Java Classloader переписать байт-код (только их копии) системных классов? - программирование
Подтвердить что ты не робот

Может ли Java Classloader переписать байт-код (только их копии) системных классов?

Итак, у меня есть classloader (MyClassLoader), который поддерживает набор "специальных" классов в памяти. Эти специальные классы динамически компилируются и хранятся в массиве байтов внутри MyClassLoader. Когда MyClassLoader запрашивает класс, он сначала проверяет, содержит ли его словарь specialClasses, прежде чем делегировать загрузку класса System. Это выглядит примерно так:

class MyClassLoader extends ClassLoader {
    Map<String, byte[]> specialClasses;

    public MyClassLoader(Map<String, byte[]> sb) {
        this.specialClasses = sb;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (specialClasses.containsKey(name)) return findClass(name);
        else return super.loadClass(name);
    }

    @Override
    public Class findClass(String name) {
        byte[] b = specialClasses.get(name);
        return defineClass(name, b, 0, b.length);
    }    
}

Если я хочу выполнить преобразования (например, инструментальные средства) в specialClasses, я могу сделать это просто, изменив byte[] до того, как я назову defineClass() на нем.

Я также хотел бы преобразовать классы, которые предоставляются загрузчиком класса System, но загрузчик классов System не предоставляет никакого доступа к исходному byte[] классам, которые он предоставляет, и дает мне Class объекты напрямую.

Я мог бы использовать инструмент -javaagent для всех классов, загружаемых в JVM, но это добавило бы накладные расходы к классам, которые я не хочу использовать; Я действительно хочу, чтобы классы, загруженные MyClassLoader, были инструментальными.

  • Есть ли способ получить исходный byte[] классов, предоставляемых родительским загрузчиком классов, поэтому я могу настроить их перед определением моей собственной копии?
  • Альтернативно, существует ли какой-либо способ эмуляции функциональных возможностей загрузчика класса System, с точки зрения того, где он захватывает его byte[], так что MyClassLoader может определять и определять свою собственную копию всех классов системы (Object, String, и др.)?

EDIT:

Итак, я попробовал другой подход:

  • Используя -javaagent, запишите byte[] каждого загружаемого класса и сохраните его в хеш-таблице, с именем имени класса.
  • MyClassLoader вместо делегирования системных классов к его родительскому загрузчику классов вместо этого загрузит свой байт-код из этой хэш-таблицы с использованием имени класса и определит его

В теории это позволит MyClassLoader определить собственную версию системных классов с инструментами. Однако с ошибкой

java.lang.SecurityException: Prohibited package name: java.lang

Ясно, что JVM не любит, чтобы я сам определял классы java.lang, хотя он должен (теоретически) быть из того же источника byte[], из которого должны были загружаться классы, загруженные с начальной загрузки. Поиск решения продолжается.

EDIT2:

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

4b9b3361

Ответ 1

Итак, я нашел решение для этого. Это не очень изящное решение, и это вызовет много сердитых писем во время проверки кода, но, похоже, это работает. Основные моменты:

JavaAgent

Используйте java.lang.instrumentation и -javaagent для хранения объекта Instrumentation для использования позже

class JavaAgent {
    private JavaAgent() {}

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Agent Premain Start");
        Transformer.instrumentation = inst;
        inst.addTransformer(new Transformer(), inst.isRetransformClassesSupported());
    }    
}

ClassFileTransformer

Добавьте Transformer в Instrumentation, который работает только с отмеченными классами. Что-то вроде

public class Transformer implements ClassFileTransformer {
    public static Set<Class<?>> transformMe = new Set<>()
    public static Instrumentation instrumentation = null; // set during premain()
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] origBytes) {


        if (transformMe.contains(classBeingRedefined)) {
            return instrument(origBytes, loader);
        } else {
            return null;
        }
    }
    public byte[] instrument(byte[] origBytes) {
        // magic happens here
    }
}

ClassLoader

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

public class MyClassLoader extends ClassLoader{
    public Class<?> instrument(Class<?> in){
        try{
            Transformer.transformMe.add(in);
            Transformer.instrumentation.retransformClasses(in);
            Transformer.transformMe.remove(in);
            return in;
        }catch(Exception e){ return null; }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return instrument(super.loadClass(name));
    }
}

... и вуаля! Каждый класс, который загружается MyClassLoader, преобразуется с помощью метода instrument(), включая все системные классы, такие как java.lang.Object и друзей, тогда как все классы, загруженные стандартным ClassLoader, остаются нетронутыми.

Я пробовал это с использованием метода памяти instrument(), который вставляет обратные вызовы для отслеживания выделения памяти в инструментальном байт-коде и может подтвердить, что классы MyClassLoad запускают обратные вызовы, когда их методы запускаются (даже система классы), а "нормальные" классы - нет.

Победа!

Это, конечно, ужасный код. Общее изменяемое состояние во всем мире, нелокальные побочные эффекты, глобалы, все, что вы можете себе представить. Наверное, тоже не потолочный. Но это показывает, что такое возможно, вы действительно можете выборочно использовать байт-код классов, даже системных классов, как часть пользовательской операции ClassLoader, оставляя при этом "отдых" программы нетронутым.

Открытые проблемы

Если у кого-то есть идеи, как сделать этот код менее страшным, я был бы рад услышать его. Я не мог понять способ:

  • Создание Instrumentation только классов инструмента по требованию через retransformClasses(), а не классов инструментов, загруженных в противном случае
  • Сохраните некоторые метаданные в каждом объекте Class<?>, который позволит Transformer определить, следует ли преобразовать его или нет, без поиска с глобальным изменчивым хеш-таблицей.
  • Преобразование системного класса без использования метода Instrumentation.retransformClass(). Как упоминалось, любые попытки динамически defineClass a byte[] в класс java.lang.* не выполняются из-за жестко запрограммированных проверок в ClassLoader.java.

Если кто-нибудь найдет способ обойти любую из этих проблем, это сделает это гораздо менее отрывочным. Несмотря на это, я предполагаю, что вы можете использовать инструмент (например, для профилирования), какую-то подсистему (то есть ту, которая вам интересна), оставив остальную часть JVM нетронутой (без каких-либо накладных расходов) будет полезной для кого-то еще, кроме меня, так вот оно.

Ответ 2

Сначала объяснение без ClassFileTransformer:

Лицензия Oracle JRE/JDK включает в себя то, что вы не можете изменять пакеты java. * и из того, что вы показали с помощью теста на попытку изменить что-то в java.lang, они включили тест и исключение безопасности, если вы попытаетесь.

С учетом сказанного вы можете изменить поведение системных классов, скомпилировав альтернативу и ссылаясь на нее с помощью опции CLI JRE -Xbootclasspath/p.

Посмотрев, что вы можете достичь с помощью этого метода, я ожидаю, что вам придется больше работать и скомпилировать пользовательскую версию OpenJDK. Я ожидаю этого, потому что загрузочный загрузчик Bootstrap (из того, что я прочитал) является собственной версией.

См. http://onjava.com/pub/a/onjava/2005/01/26/classloading.html для моего любимого обзора загрузчиков классов.

Теперь с ClassFileTransformer:

Как вы показали, вы можете обновить программы методов (и некоторые конкретные другие аспекты предварительно загруженных классов). В ответ на заданные вами вопросы:

Прибор по требованию: что важно здесь, что каждый загруженный класс имеет уникальный экземпляр класса, связанный с ним; поэтому, если вы хотите настроить таргетинг на определенный загруженный класс, вам нужно будет указать, какой именно экземпляр существует, и это можно найти различными способами, включая член "класс", связанный с каждым именем класса, например Object.class.

Является ли он потокобезопасным: нет, два потока могут одновременно изменять набор, и вы можете решить эту проблему разными способами; Я предлагаю использовать параллельную версию Set.

Глобалы и т.д.: Я думаю, что глобальные особенности необходимы (я думаю, что ваша реализация может быть сделана немного лучше), но, скорее всего, проблем не будет, и вы узнаете, как лучше кодировать Java позже (I ' ve закодированы около 12 лет, и вы не поверили бы в некоторые тонкие вещи об использовании языка).

Метаданные в экземплярах класса: все время, когда я использовал Java, добавление метаданных не было естественным и, вероятно, не без оснований; сохранение карты для конкретной цели является прекрасным, и помните, что это только карта между указателем на экземпляр и метаданные, поэтому это не настоящий бог памяти.

Ответ 3

Погрузчик класса не предлагает публичный API для доступа к байтовому коду уже загруженных классов. Байт-код, скорее всего, кэшируется где-то с помощью VM (в Oracle VM это делается в собственном коде), но вы больше не можете получить его как байтовый массив.

Однако вы можете перечитать файл класса в качестве ресурса. Если я не забуду что-то очевидное, ClassLoader # getResource() или Class # getResource() должен использовать один и тот же путь поиска для загрузки файлов классов, так как он используется для загрузки ресурсов:

public byte[] getClassFile(Class<?> clazz) throws IOException {     
    InputStream is =
        clazz.getResourceAsStream(
            "/" + clazz.getName().replace('.', '/') + ".class");
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int r = 0;
    byte[] buffer = new byte[8192];
    while((r=is.read(buffer))>=0) {
        baos.write(buffer, 0, r);
    }   
    return baos.toByteArray();
}

Ответ 4

Вы хотите переопределить Object.class, но этот класс уже загружен до запуска любой программы, включая ваш загрузчик классов. Даже если вы создадите свой собственный Object.class, он будет конфликтовать с уже загруженным системным классом, и это создаст беспорядок.

Единственный способ, с помощью которого я вижу, - отключить системные классы, т.е. взять rt.jar, задействовать все классы в нем и написать обратно.