Статьи

Практическая инженерия байт-кода

За последние несколько лет я написал несколько блогов о том, как использовать разработку байт-кода. Моя первая статья была кратким обзором, в то время как другие обсуждали конкретные тематические исследования. Оглядываясь назад, я думаю, что упустил из виду основные строительные блоки разработки байт-кода: Java-агент и Instrumentation API. Кроме того, могут быть полезны некоторые загружаемые и практичные примеры проектов по разработке байт-кода. Эта статья направлена ​​на решение этих проблем.

Существует два основных способа инструментирования байт-кода Java. Один из способов — изменить целевые классы до времени выполнения, а затем соответствующим образом настроить ваш путь к классам (и, возможно, путь к загрузке), чтобы он указывал на ваши инструментированные классы. К счастью, (начиная с Java 1.5), существует специальный Java API для инструментария (среди других вещи) называется JVM TI (Tooling Interface). JVM TI позволяет вам присоединять нативные или Java- агенты. Этот блог будет посвящен только агентам Java (я говорю людям, что предпочитаю их из-за переносимости платформы, но правда в том, что мои навыки программирования на C действительно устарели).

Модуль развертывания агента Java представляет собой файл JAR. Файл jar должен иметь манифест, созданный для поддержки агентов. Вы можете обратиться к документации пакета инструментов для деталей для манифеста и других требований, но вот сокращенная версия атрибутов манифеста:

  • Premain-Class : требуется для поддержки подключения агентов при запуске JVM . Вы можете думать об этом как о классе, содержащем «public static final void main», который используется для вызова любой Java-программы.
  • Класс агента : требуется для поддержки динамического подключения агентов после того, как JVM уже запущена . Опять же, вы можете думать об этом как о классе, содержащем «public static final void main», который используется для вызова любой Java-программы.

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

  • Boot-Class-Path : я никогда не определяю это свойство. Если я хочу манипулировать путем загрузки, я предпочитаю делать это с помощью API инструментария после загрузки агента.
  • Can-Redefine-Classes : индикатор того, что этот агент может вызывать Instrumentation.redefineClasses (…). «Переопределение» применяется к классам, которые уже были загружены.
  • Can-Retransform-Classes : индикатор того, что этот агент может вызывать Instrumentation.retransformClasses (…) «Retransformation» применяется к классам по мере их загрузки.
  • Can-Set-Native-Method-Prefix : Этот блог посвящен не родным агентам, но если вы заинтересованы, вы можете получить подробное описание этого свойства в документации по методу Instrumentation.setNativeMethodPrefix. Далее, давайте посмотрим на агента: точка входа ». Как указано в разделе Интерфейс командной строки javadocs пакета java.lang.instrumentation , имя метода точки входа должно быть либо «premain» для агентов, которые подключены при первоначальном вызове JVM, либо «agentmain» для агентов, которые подключены после начала JVM. Вот действительные сигнатуры методов:
  • public static void premain (String agentArgs, Instrumentation inst);
  • public static void premain (String agentArgs);
  • public static void agentmain (String agentArgs, Instrumentation inst);
  • public static void agentmain (String agentArgs);

JVM попытается сначала вызвать аромат с параметром Instrumentation, а другой вызовет только, если первый не существует.

Для вызова агента во время инициализации JVM вы указываете следующий параметр JVM:

-javaagent:<path to agent jar>=<agent arguments>

Присоединить агент к работающей JVM немного сложнее. Метод VirtualMachine.attach (String pid) APIинтерфейса Attach позволяет одной JVM подключаться к другой. После подключения методы VirtualMachine.loadAgent (…) можно использовать для загрузки агента в JVM, к которой вы подключены. Следует отметить, что параметр «pid» метода присоединения является идентификатором процесса целевой JVM. Если вы знаете PID целевой JVM, вы можете использовать следующий (упрощенный) код для присоединения:

public static void main(String args[]) throws Exception {
    VirtualMachine.attach(args[0]).loadAgent(args[1]);
}

Предполагая, что этот метод находится в классе ca.discotek.attachexample.Attacher, вы должны вызвать этот код следующим образом:

java ca.discotek.attachexample.Attacher 1234 C:/agentdir/myagent.jar

Обратите внимание, что Attach API является частью tools.jar, который доступен только в JDK.

Возвращаясь к построению класса агента, если вы хотите выполнить какие-либо преобразования класса, вам необходимо реализовать интерфейс java.lang.instrument.ClassFileTransfomer , который определяет следующий метод:

byte[] transform(ClassLoader loader,
                 String className,
                 Class<?> classBeingRedefined,
                 ProtectionDomain protectionDomain,
                 byte[] classfileBuffer)
                 throws IllegalClassFormatException;

Этот метод возвращает байтовый массив, который содержит определение байтового кода для данного класса. Вот некоторые примечания относительно параметров:

  1. loader : будет нулевым, если ClassLoader является загрузчиком ClassLoader, поэтому не думайте, что он не будет нулевым!
  2. className : Хотите верьте, хотите нет, но иногда это значение может быть нулевым, поэтому не думайте, что оно будет ненулевым! Кроме того, он всегда будет использовать косую черту в качестве разделителя имен пакетов / классов (например, java / lang / String , а не java.lang.String ).
  3. classBeingRedefined : будет нулевым, если класс загружается впервые, поэтому не думайте, что он не будет нулевым!
  4. protectionDomain : я никогда не использую этот параметр, поэтому мне нечего сказать по этому поводу.
  5. classfileBuffer : это никогда не будет ненулевым!

Ваш класс агента не обязательно должен быть тем классом, который реализует этот интерфейс, но ваш код класса агента, вероятно, будет отвечать за активацию любой реализации ClassFileTransformer путем вызова instrumentation.addTransformer (…). Этот метод поставляется в двух вариантах:

public void addTransformer(ClassFileTransformer transformer) 
public void addTransformer(ClassFileTransformer transformer, boolean canRetransform)

Эти методы зарегистрируют ClassFileTransformer в JVM. Второй метод содержит логический параметр, который отмечает преобразователь как заинтересованный в обработке байт-кода класса при загрузке. ClassFileTransformer, который добавляется без установки этого параметра в значение true, не сможет преобразовать байтовый код. Будет вызван его метод преобразования, но массив байтового кода, возвращенный этим методом, будет просто отброшен.

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

agent-example-0-basic [ скачать ]

Создает jar агента, который просто печатает все имена классов и их загрузчики классов по мере их загрузки:

    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println("Basic Agent: " + className + " : " + loader);

        return null;
    }

Чтобы увидеть это в действии

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу BasicTest (тестируется в корне проекта) со следующим параметром JVM: -javaagent: <путь к проекту> /dist/myagent.jar

agent-example-1-attach [ Скачать ]

В этом проекте не так много кода, но он несколько сложен. Он демонстрирует, как вы можете подключить агент к работающей JVM. Давайте сначала посмотрим на класс агента. Мы подключаемся к JVM, поэтому нам нужен только метод agentmain в нашем классе агента ca.discotek.agent.example.attache.MyAgent :

    public static void agentmain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, false);
    }

Давайте теперь посмотрим на метод инициализации :

    public static void initialize(String agentArgs, Instrumentation inst, boolean isPremain) {
        MyAgent.instrumentation = inst;
        inst.addTransformer(new MyClassFileTransformer(), true);

        Runnable r = new Runnable() {
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        Class classes[] = instrumentation.getAllLoadedClasses();
                        for (int i=0; i<classes.length; i++) {
                            if (classes[i].getName().equals("ca.discotek.agent.example.attach.test.AttachTest")) {
                                System.out.println("Reloading: " + classes[i].getName());
                                instrumentation.retransformClasses(classes[i]);
                                break;
                            }
                        }

                    } 
                    catch (Exception e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        Thread t = new Thread(r);
        t.start();
    }

Метод initialize делает две основные вещи:

  1. Вторая строка регистрирует новый экземпляр MyClassFileTransformer как ClassFileTransformer с JVM с использованием метода Instrumentation.addTransformer (…) .
  2. Создает некоторый зацикливающийся код в потоке, который будет непрерывно планировать преобразование класса AttachTest .

Далее у нас есть класс ca.discotek.agent.example.attache.MyClassFileTransformer . Он реализует интерфейс ClassFileTransformer и имеет следующий метод преобразования :

    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className != null && className.startsWith("ca/discotek/"))
            System.out.println("Attach Agent: " + className);

        return null;
    }

Этот метод не делает ничего, кроме распечатки имени любых классов, которые загружаются / преобразуются, которые начинаются с ca / discotek / . Это не особенно интересно, но оно продемонстрирует, что агент MyAgent был подключен и успешно добавил MyClassFileTransformer , который постоянно перезагружает классы.

У нас также есть класс ca.discotek.agent.example.attach.Attacher с одним методом:

    public static void main(String[] args) throws Exception {
        VirtualMachine.attach(args[0]).loadAgent(args[1]);
    }

Этот класс принимает PID процесса Java в качестве первого параметра и путь к jar агента в качестве второго параметра.

Наконец, у нас есть ca.discotek.agent.example.attach.test.AttachTest , который просто используется для создания JVM для демонстрации функциональности присоединения / агента:

    public static void main(String[] args) throws Exception {
        while (true) {
            System.out.println("Sleeping...");
            Thread.sleep(1000);
        }
    }

Чтобы увидеть этот агент в действии

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу AttachTest. Вы можете запустить это из вашей IDE или из командной строки.
  3. Запустите программу Attacher (в том же пакете, что и MyAgent) со следующими параметрами <pid> <путь к проекту> /dist/myagent.jar, где вам потребуется определить pid с помощью таких инструментов, как jps, jconsole или диспетчер процессов операционной системы. , Помните, что вам нужно иметь файл tools.jar JDK в вашем пути к классам. Командная строка будет выглядеть примерно так:
java -classpath .../discotek-agent-example-1-attach/bin;/java/jdkx.y.z/lib/tools.jar ca.discotek.agent.example.attach.Attacher 16948 .../discotek-agent-example-1-attach/dist/myagent.jar

Вы можете подтвердить, что правильно подключили к работающей JVM, когда класс MyClassFileTransformer неоднократно печатает следующее:

Sleeping...
Reloading: ca.discotek.agent.example.attach.test.AttachTest
Attach Agent: ca/discotek/agent/example/attach/test/AttachTest

агент-пример-2-access-javassist [ скачать ]

Создает jar агента, который будет использовать Javassist для изменения модификаторов доступа тестового класса с приватного на публичный . Он имеет реализацию метода ClassFileTransformer.transform () следующим образом:

    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {

        String dotName = className.replace('/', '.');
        if (className != null && transformPattern.matcher(dotName).matches()) {

            try {
                ClassPool pool = ClassPool.getDefault();
                pool.appendClassPath(new ByteArrayClassPath(dotName, classfileBuffer));
                CtClass ctClass = pool.get(dotName);
                int modifiers = ctClass.getModifiers();
                ctClass.setModifiers(Modifier.setPublic(modifiers));

                CtField ctField = ctClass.getDeclaredField("privateMessage");
                modifiers = ctField.getModifiers();
                ctField.setModifiers(Modifier.setPublic(modifiers));

                CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{});
                modifiers = ctConstructor.getModifiers();
                ctConstructor.setModifiers(Modifier.setPublic(modifiers));

                CtMethod ctMethod = ctClass.getDeclaredMethod("printMessage");
                modifiers = ctMethod.getModifiers();
                ctMethod.setModifiers(Modifier.setPublic(modifiers));

                return ctClass.toBytecode(); 
            } 
            catch (Exception e) {
                throw new RuntimeException("Bug", e);
            }

        }

        return null;
    }

Проект также тест исходной папки , которая содержит классы для тестирования агента. Эти классы:

ca.discotek.agent.example.access.javassist.testee.PrivateTest , который имеет методы:

    private String privateMessage = "This is a private message";

    private PrivateTest() {
        System.out.println("Private Constructor");
    }

    private void printMessage() {
        System.out.println("This is a private method");
    }

Этот класс имеет приватное поле, приватный конструктор и приватный метод. Сам класс тоже частный.

Папка исходного кода также имеет класс ca.discotek.agent.example.access.javassist.tester.AccessJavassistTest с основным методом:

    public static void main(String[] args) throws Exception {

        Class c = null;
        try {
            c = Class.forName("ca.discotek.agent.example.access.javassist.testee.PrivateTest");
            System.out.println("Class is public? " + Modifier.isPublic(c.getModifiers()) );
        } 
        catch (Throwable t) {
            t.printStackTrace();
        }

        Object o = null;
        try {
            Constructor constructor = c.getDeclaredConstructor(new Class[]{});
            System.out.println("Constructor is public? " + Modifier.isPublic(constructor.getModifiers()) );
            o = constructor.newInstance(new Object[]{});
        } 
        catch (Throwable t) {
            t.printStackTrace();
        }

        try {
            Field field = c.getField("privateMessage");
            System.out.println("Field is public? " + Modifier.isPublic(field.getModifiers()) );
            Object value  = field.get(o);
            System.out.println("Field value: " +  value);
        } 
        catch (Throwable t) {
            t.printStackTrace();
        }

        try {
            Method method = c.getMethod("printMessage", new Class[]{});
            System.out.println("Method is public? " + Modifier.isPublic(method.getModifiers()) );
            method.invoke(o, new Object[]{});
        } 
        catch (Throwable t) {
            t.printStackTrace();
        }
    }

Этот метод проверяет , доступны ли какие-либо частные объекты в классе PrivateTest .

Чтобы увидеть этого агента в действии:

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу ca.discotek.agent.example.access.javassist.tester.AccessJavassistTest . Вы можете запустить это из вашей IDE или из командной строки. Вам нужно будет добавить параметр -javaagent . Вот как это может выглядеть:
java -javaagent:.../discotek-agent-example-2-access-javassist/dist/agent-access-javassist.jar=.*discotek.*test.* -classpath .../discotek-agent-example-2-access-javassist/bin;.../discotek-agent-example-2-access-javassist/lib/javassist.jar ca.discotek.agent.example.access.javassist.tester.AccessJavassistTest

Это предполагает, что ваша IDE компилирует классы в каталог bin в корне вашего проекта (в противном случае -classpath в приведенном выше примере будет неправильным).

агент-пример-3-access-asm [ скачать ]

Создает jar агента, который будет использовать ASM для изменения модификаторов доступа тестового класса с частного на публичный . Он имеет реализацию метода ClassFileTransformer.transform () следующим образом:

    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className != null && transformPattern.matcher(className.replace('/', '.')).matches()) {
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            AccessClassVisitor accessClassVisitor = new AccessClassVisitor(cw);
            ClassReader cr = new ClassReader(classfileBuffer);
            cr.accept(accessClassVisitor, ClassReader.SKIP_FRAMES);

            return cw.toByteArray();
        }

        return null;
    }

У нас также есть AccessClassVisitor, который используется для выполнения изменений байтового кода:

public class AccessClassVisitor extends ClassVisitor {

    static int convertToPublicAccess(int access) {
        access &= ~Opcodes.ACC_PRIVATE;
        access &= ~Opcodes.ACC_PROTECTED;
        access |= Opcodes.ACC_PUBLIC;
        return access;
    }

    public AccessClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, convertToPublicAccess(access), name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access,
            String name,
            String desc,
            String signature,
            String[] exceptions) {    

        return super.visitMethod(convertToPublicAccess(access), name, desc, signature, exceptions);
    }

    @Override
    public FieldVisitor visitField(int access,
            String name,
            String desc,
            String signature,
            Object value) {
        return super.visitField(convertToPublicAccess(access), name, desc, signature, value);
    }
}

Чтобы увидеть этого агента в действии:

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу ca.discotek.agent.example.access.asm.tester.AccessAsmTest . Вы можете запустить это из вашей IDE или из командной строки. Вам нужно будет добавить параметр -javaagent . Вот как это может выглядеть:
java -noverify -javaagent:.../discotek-agent-example-3-access-asm/dist/agent-access-asm.jar=.*discotek.*test.* -classpath .../discotek-agent-example-3-access-asm/bin;.../discotek-agent-example-3-access-asm/lib/asm-5.0.4.jar ca.discotek.agent.example.access.asm.tester.AccessAsmTest

Это предполагает, что ваша IDE компилирует классы в каталог bin в корне вашего проекта (в противном случае -classpath в приведенном выше примере будет неправильным).

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

agent-example-4-asm-internal-types [ Загрузить ]

Этот следующий проект не является сборкой агента. Он создает программу, которая может помочь любому, кто пытается правильно указать внутренние дескрипторы JVM. Правильное понимание внутреннего дескриптора JVM очень важно для разработки байт-кода и
очень часто используется с ASM. Они также используются Javassist, но не так часто. Главный пример Javassist я могу думать о том CtClass «s getConstructor (String дескриптор) и getMethod (имя String, String дескриптор) метода.

Я не буду вставлять соответствующий код здесь, но вот снимок экрана в действии. Здесь он загрузил jar, собранный путем запуска сборки по умолчанию сценария ANT в корне этого проекта.

Чтобы увидеть этот код в действии, либо:

  • Запустите код непосредственно из вашей IDE, используя класс точки входа ca.discotek.agent.example.internaltypes.InternalTypeConverterView .
    1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
    2. Затем вызовите программу, используя команду типа: java -classpath… / discotek-agent-example-4-asm-internal-types / dist / internal-types.jar ca.discotek.agent.example.internaltypes.InternalTypeConverterView

агент-пример-5-objectsize-asm [ скачать ]

Вместо того байта кода преобразований, этот следующий проект использует Instrumentation «ы GetObjectSize (Объект O) метод для вычисления размера произвольного объекта в памяти.

Вот снимок экрана с конечным продуктом:

В верхней части графического интерфейса есть таблица с четырьмя столбцами:

  1. Тип : есть строка для каждого базового типа Java. Каждый класс, который вы определяете, будет содержать некоторую комбинацию этих типов (возможно, ни один)
  2. Count : Этот столбец представляет собой число полей из типа , указанного для данной строки , которые будут отображаться в произвольном классе
  3. Размер шрифта: Этот столбец является размером в байтах для Типа в каждой строке
  4. Вычисляемая промежуточная сумма : в этом столбце вычисляется размер, который будут использовать типы для данной строки (т. Е. Количество * Размер шрифта )

Чуть ниже таблицы расположены кнопки увеличения и уменьшения . Эти кнопки будут увеличивать или уменьшать число в Count колонке для данной строки.

В нижней части таблицы, есть текстовое поле, которое показывает расчетную общий размер объекта и размер , как возвращение Instrumentation «ы GetObjectSize (о) Объект метода.

Код в классе агента очень прост:

    public static void premain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, true);
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, false);
    }

    public static void initialize(String agentArgs, Instrumentation inst, boolean isPremain) {
        try {  ObjectSizeAnalyzerView.showObjectAnalyzerView(inst); } 
        catch (Exception e) { e.printStackTrace(); }
    }

Большая часть кода ObjectSizeAnalyzeView тоже не очень интересна. Однако ASM используется для генерации объекта с полями, указанными в таблице, что стоит отметить. Один метод updateSize () используется для создания нового класса с указанными полями, а затем создания экземпляра этого класса. Затем он передает этот объект в качестве параметра в инструментарий . GetObjectSize (Объект O) метод

    void updateSize() {

        String className = "MyClass" + classIndex++;

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);

        Method m = Method.getMethod("void()");
        GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, m, null, null, cw);
        mg.loadThis();
        mg.invokeConstructor(Type.getType(Object.class), m);
        mg.returnValue();
        mg.endMethod();

        int fieldIndex = 0;
        Type type;
        int count;
        String desc;
        for (int i=0; i<TYPES.length; i++) {
            count = model.countList.get(i);
            for (int j=0; j<count; j++) {
                type = Type.getType(TYPE_CLASSES[i]);
                desc = getDescriptor(type);
                cw.visitField(Opcodes.ACC_PUBLIC, "field" + fieldIndex++, desc, null, null);
            }
        }

        cw.visitEnd();

        MyClassLoader loader = new MyClassLoader(className, cw.toByteArray());
        try {
            Class c = loader.loadClass(className);
            currentObject = c.newInstance();
            long objectSize = instrumentation.getObjectSize(currentObject);
            StringBuilder buffer = new StringBuilder();
            long calculated = 8;
            Integer ints[] = model.countList.toArray(new Integer[model.countList.size()]);
            for (int i=0; i

Чтобы увидеть этого агента в действии:

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу ca.discotek.agent.example.objectsize.asm.test.ObjectSizeAsmTest . Вы можете запустить это из вашей IDE или из командной строки. Вам нужно будет добавить параметр -javaagent . Вот как это может выглядеть:
java -javaagent:.../discotek-agent-example-5-objectsize-asm/dist/agent-objectsize-asm.jar -classpath .../discotek-agent-example-5-objectsize-asm/bin;.../discotek-agent-example-5-objectsize-asm/lib/asm-5.0.4.jar;.../discotek-agent-example-5-objectsize-asm/lib/asm-commons-5.0.4.jar ca.discotek.agent.example.objectsize.asm.test.ObjectSizeAsmTest

агент-пример-6-профиль-javassist [ скачать ]

Этот проект производит агент jar, который демонстрирует один из недостатков Javassist. В частности, если добавить локальную переменную с помощью CtBehaviour «s InsertBefore метод, вы можете не позднее ссылаться на него в коде добавлены с помощью CtBehaviour » s InsertAfter метод.
Javassist ничего не знает о байт-коде, который вы ранее вставили. Давайте посмотрим, что произойдет, когда мы используем Javassist для создания агента для методов инструментов, чтобы профилировать время выполнения. Агент будет применять все методы в классах, чье имя соответствует регулярному выражению, указанному в аргументах агента. Вот как агент инициализируется:

    public static void premain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, false);
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, true);
    }

    static void initialize(String agentArgs, Instrumentation inst, boolean isPremain) {
        MyAgent.instrumentation = inst;
        inst.addTransformer(new MyAgent(Pattern.compile(agentArgs)), true);
    }

    public MyAgent(Pattern transformPattern) {
        this.transformPattern = transformPattern;
    }

Метод transform отфильтровывает любые нежелательные классы и передает байт-код в метод прибора (…) для обработки:

    void instrument(CtBehavior behaviour) throws CannotCompileException {
        String beforeCode = "long __start_time__ = System.currentTimeMillis();";
        behaviour.insertBefore(beforeCode);

        StringBuilder buffer = new StringBuilder();
        buffer.append("long __end_time__ = System.currentTimeMillis();");
        String method = "$class" + '.' + behaviour.getName() + behaviour.getSignature();
        buffer.append("System.out.println(\"Ellapsed " + method + ": \" + (__end_time__ - __start_time__));");

        String afterCode = buffer.toString();
        behaviour.insertAfter(afterCode, true);

        behaviour.addLocalVariable("__start_time__", CtClass.longType);
        behaviour.addLocalVariable("__end_time__", CtClass.longType);
    }

Несмотря на то, что этот код будет вызывать ошибку Javassist во время выполнения, вот некоторые моменты, на которые стоит обратить внимание:

  • Метод instrument (…) принимает объект CtBehaviour в качестве параметра. CtBehaviour это супер класс как CtConstructor и CtMethod , так что он может обрабатывать либо.
  • Конструктор — это специальный тип методов, используемых для инициализации объекта. Как таковое, у него есть правило, что не должно быть никаких инструкций байтового кода для вызова каких-либо методов в конструкторе, предшествующем вызову конструкторов super . К счастью, Javassist’s CtBehaviour . insertBefore знает и вставит ваш код после вызова super .
  • Вы заметите, что имена локальных переменных, вставленных в каждый метод (например, __start_time__, __end_time__), выглядят немного странно. Если вы вставляете какую-либо конструкцию в байт-код, с которым вы не знакомы, вам нужно убедиться, что вы не нарушаете правила об уникальности. В этом случае у вас не может быть двух локальных переменных с одинаковым именем. Если бы мы использовали «start» в качестве имени переменной вместо «__start_time__», более вероятно, что может произойти конфликт. Я обычно включаю «дискотек» в название переменной, чтобы обеспечить уникальность.

И наконец, вот наш простой тестовый клиентский код:

public class ProfileJavassistTest {

    public static final void main(String[] args) throws Exception {
        System.out.println("Jon Snow, Ned Stark's bastard, likes to say \"Winter is coming.\"");
    }
}

Чтобы увидеть этого агента в действии:

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу ca.discotek.agent.example.profile.javassist.test.ProfileJavassistTest . Вы можете запустить это из вашей IDE или из командной строки. Вам нужно будет добавить параметр -javaagent . Вот как это может выглядеть:
java -noverify -javaagent:.../discotek-agent-example-6-profile-javassist/dist/agent-profile-javassist.jar=.*discotek.*test.* -classpath .../discotek-agent-example-6-profile-javassist/bin;.../discotek-agent-example-6-profile-javassist/lib/javassist.jar ca.discotek.agent.example.profile.javassist.test.ProfileJavassistTest

Вот как будет выглядеть вывод:

javassist.CannotCompileException: [source error] no such field: __start_time__
    at javassist.CtBehavior.insertAfter(CtBehavior.java:815)
    at ca.discotek.agent.example.profile.javassist.MyAgent.instrument(MyAgent.java:79)
    at ca.discotek.agent.example.profile.javassist.MyAgent.transform(MyAgent.java:54)
    at sun.instrument.TransformerManager.transform(Unknown Source)
    at sun.instrument.InstrumentationImpl.transform(Unknown Source)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.security.SecureClassLoader.defineClass(Unknown Source)
    at java.net.URLClassLoader.defineClass(Unknown Source)
    at java.net.URLClassLoader.access$100(Unknown Source)
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
Caused by: compile error: no such field: __start_time__
    at javassist.compiler.TypeChecker.fieldAccess(TypeChecker.java:812)
    at javassist.compiler.TypeChecker.atFieldRead(TypeChecker.java:770)
    at javassist.compiler.TypeChecker.atMember(TypeChecker.java:952)
    at javassist.compiler.JvstTypeChecker.atMember(JvstTypeChecker.java:65)
    at javassist.compiler.ast.Member.accept(Member.java:38)
    at javassist.compiler.TypeChecker.atBinExpr(TypeChecker.java:328)
    at javassist.compiler.ast.BinExpr.accept(BinExpr.java:40)
    at javassist.compiler.TypeChecker.atPlusExpr(TypeChecker.java:370)
    at javassist.compiler.TypeChecker.atBinExpr(TypeChecker.java:311)
    at javassist.compiler.ast.BinExpr.accept(BinExpr.java:40)
    at javassist.compiler.JvstTypeChecker.atMethodArgs(JvstTypeChecker.java:220)
    at javassist.compiler.TypeChecker.atMethodCallCore(TypeChecker.java:702)
    at javassist.compiler.TypeChecker.atCallExpr(TypeChecker.java:681)
    at javassist.compiler.JvstTypeChecker.atCallExpr(JvstTypeChecker.java:156)
    at javassist.compiler.ast.CallExpr.accept(CallExpr.java:45)
    at javassist.compiler.CodeGen.doTypeCheck(CodeGen.java:241)
    at javassist.compiler.CodeGen.atStmnt(CodeGen.java:329)
    at javassist.compiler.ast.Stmnt.accept(Stmnt.java:49)
    at javassist.compiler.Javac.compileStmnt(Javac.java:568)
    at javassist.CtBehavior.insertAfterHandler(CtBehavior.java:914)
    at javassist.CtBehavior.insertAfter(CtBehavior.java:778)
    ... 17 more
Jon Snow, Ned Stark's bastard, likes to say "Winter is coming."

Здесь мы видим, что когда мы пытались добавить код с помощью метода insertAfter , он не смог найти локальную переменную __start_time__, добавленную в метод insertBefore . Это колоссальная боль в шее. Прелесть Javassist в том, что вы можете вставлять настоящий код Java без особого понимания байт-кода.

agent-example-7-profile-asm [ Загрузить ]

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

    public static void premain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, true);
    }

    public static void agentmain(String agentArgs, Instrumentation inst) {
        initialize(agentArgs, inst, false);
    }

    static void initialize(String agentArgs, Instrumentation inst, boolean isPremain) {
        MyAgent.instrumentation = inst;
        inst.addTransformer(new MyAgent(Pattern.compile(agentArgs)), true);
    }

    public MyAgent(Pattern transformPattern) {
        this.transformPattern = transformPattern;
    }

И вот метод преобразования :

    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className != null && transformPattern.matcher(className.replace('/', '.')).matches()) {
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            ProfileClassVisitor accessClassVisitor = new ProfileClassVisitor(cw);
            ClassReader cr = new ClassReader(classfileBuffer);
            cr.accept(accessClassVisitor, ClassReader.SKIP_FRAMES);

            return cw.toByteArray();
        }

        return null;
    }

Метод transform передает обработку байтового кода классу ProfileClassVisitor:

public class ProfileClassVisitor extends ClassVisitor {

    String classDotName;

    public ProfileClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.classDotName = name.replace('/', '.');
    }

    @Override
    public MethodVisitor visitMethod(int access,
            String name,
            String desc,
            String signature,
            String[] exceptions) {    

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        return new ProfileMethodVisitor(mv, access, name, desc);
    }

    class ProfileMethodVisitor extends AdviceAdapter {

        String methodName = null;
        String desc = null;

        int startTimeVar = -1;

        Label timeStart = new Label();
        Label timeEnd = new Label();

        Label finallyStart = new Label();
        Label finallyEnd = new Label();

        String signature = null;

        public ProfileMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
            super(Opcodes.ASM5, mv, access, name, desc);
            this.methodName = name;
            this.desc = desc;

            signature = classDotName + '.' + methodName + toParameterString(desc); 
        }

        String toParameterString(String desc) {
            Type methodType = Type.getMethodType(desc);
            StringBuilder buffer = new StringBuilder();

            buffer.append('(');

            Type argTypes[] = methodType.getArgumentTypes();
            for (int i=0; i<argTypes.length; i++) {
                buffer.append(argTypes[i].getClassName());

                if (i<argTypes.length-1)
                    buffer.append(", ");
            }

            buffer.append(')');

            return buffer.toString();
        }

        public void visitCode() {
            super.visitCode();

            visitLabel(timeStart);
            startTimeVar = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, startTimeVar);

            visitLabel(finallyStart);
        }

        protected void onMethodExit(int opcode) {
            if (opcode != ATHROW)  
                onFinally(opcode); 
        }

        private void onFinally(int opcode) {
            if (opcode == ATHROW)
                mv.visitInsn(Opcodes.DUP);
            else
                mv.visitInsn(Opcodes.ACONST_NULL);

            int throwableVarIndex = newLocal(Type.getType(Throwable.class));
            mv.visitVarInsn(Opcodes.ASTORE, throwableVarIndex);

            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn(signature + ": ");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);

            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(Opcodes.LLOAD, startTimeVar);
            mv.visitInsn(Opcodes.LNEG);
            mv.visitInsn(LADD);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(J)V", false);

            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("ms");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }

        public void visitMaxs(int stack, int locals) { 
            Label endFinally = new Label(); 
            mv.visitTryCatchBlock(finallyStart, endFinally, endFinally, null); 
            mv.visitLabel(endFinally); 
            onFinally(ATHROW); 
            mv.visitInsn(ATHROW);
            visitLabel(timeEnd);
            visitLocalVariable("_time_", Type.LONG_TYPE.getDescriptor(), null, timeStart, timeEnd, startTimeVar);

            super.visitMaxs(stack, locals); 
        }
    }
}

У нас также есть тестовые классы ProfileAsmTest :

public class ProfileAsmTest {

    public static void main(String[] args) throws Exception {
        ProfileTest test = new ProfileTest();
        test.sleep();
    }
}

… И ProfileTest :

public class ProfileTest {

    static final long DEFAULT_SLEEP = 500;

    static {
        System.out.println("in static initializer");
    }

    public ProfileTest() throws InterruptedException {
        sleep(100);
    }

    public void sleep() throws InterruptedException {
        sleep(DEFAULT_SLEEP);
    }

    public void sleep(long sleep) throws InterruptedException {
        Thread.sleep(sleep);
    }
}

Чтобы увидеть этого агента в действии:

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу ca.discotek.agent.example.profile.asm.test.ProfileAsmTest . Вы можете запустить это из вашей IDE или из командной строки. Вам нужно будет добавить параметр -javaagent . Вот как это может выглядеть:
java -noverify -javaagent:.../discotek-agent-example-7-profile-asm/dist/agent-profile-asm.jar=.*discotek.*test.* -classpath .../discotek-agent-example-7-profile-asm/bin;.../discotek-agent-example-7-profile-asm/lib/asm-5.0.4.jar;.../discotek-agent-example-7-profile-asm/lib/asm-commons-5.0.4.jar ca.discotek.agent.example.profile.asm.test.ProfileAsmTest

Вывод должен выглядеть так:

in static initializer
ca.discotek.agent.example.profile.asm.test.ProfileTest.<clinit>(): 0ms
ca.discotek.agent.example.profile.asm.test.ProfileTest.sleep(long): 100ms
ca.discotek.agent.example.profile.asm.test.ProfileTest.<init>(): 100ms
ca.discotek.agent.example.profile.asm.test.ProfileTest.sleep(long): 501ms
ca.discotek.agent.example.profile.asm.test.ProfileTest.sleep(): 501ms
ca.discotek.agent.example.profile.asm.test.ProfileAsmTest.main(java.lang.String[]): 607ms

agent-example-8-profile-classpath-asm [ скачать ]

Этот проект почти функционально такой же, как и предыдущий, но иллюстрирует, как агент может быть собран и развернут в реалистичном сценарии. Во-первых, давайте признаем, что jar-файлы агентов появляются на пути к классам JVM. Это создает проблему, когда код приложения, запущенный в целевой JVM, конфликтует с именем класса с кодом в вашем агенте. Например, агент в предыдущем примере использует ASM, но ASM использует большинство современных серверов приложений и веб-контейнеров. Если ASM появится в пути к классам раньше, чем ваш агент, классы ASM приложения будут загружены до классов ASM агента. Эта проблема довольно легко решается путем повторного добавления кода ASM. Вместо ссылки на класс ASM в его оригинальном jar (как и в предыдущем проекте), исходный код ASM (который является открытым исходным кодом) может быть добавлен к самому проекту.Затем его можно изменить, чтобы дать каждому классу уникальное пространство имен. В этом проекте путь пакета ASMorg.objectweb.asm реорганизован в ca.discotek.example.org.objectweb.asm . ca.discotek дает пути к пакету имя, которое является уникальным для организации discotek.ca, а ca.discotek.example дает пути к пакету имя, которое не является обязательным для агента. Это необходимо, когда в организации имеется несколько агентов, которые могут быть установлены в одной JVM.

Вторая проблема возникает, когда агенту необходимо вставить новые классы в область приложения. Предыдущий пример не делает этого, но воображение не может понять, как оно может. Вместо вызова System.out.println (…) для вывода результатов профилирования метода, возможно, было бы более полезно использовать некоторый API для записи результатов централизованно. Для этого в этом проекте добавлен класс ca.discotek.agent.example.profileclasspath.Recorder :

public class Recorder {

    public static void record(String methodSignature, long start, long end) {
        StringBuilder buffer = new StringBuilder();
        buffer.append(methodSignature);
        buffer.append(": ");
        buffer.append(end - start);
        buffer.append("ms");
        System.out.println(buffer);
    }
}

Этот класс имеет единственный метод записи, который использует System.out.println (…) для вывода результатов, но его можно легко изменить, чтобы сохранить результаты в базе данных. Теперь нам нужно немного изменить наш код инструментария, чтобы вызвать Recorder.record (…) . Последняя строка метода onFinally теперь вызывает этот метод:

        private void onFinally(int opcode) {
            if (opcode == ATHROW)
                mv.visitInsn(Opcodes.DUP);
            else
                mv.visitInsn(Opcodes.ACONST_NULL);

            mv.visitLdcInsn(signature);
            mv.visitVarInsn(Opcodes.LLOAD, startTimeVar);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, Recorder.class.getName().replace('.', '/'), "record", "(Ljava/lang/String;JJ)V", false);
        }

Давайте теперь вернемся к обсуждению проблемы со вставкой новых классов в пространство приложения. В этом сценарии мы модифицируем методы приложения для вызова Recorder.record (…) . Если мы связываем класс Recorder непосредственно в jar агента, он будет обнаружен любым загрузчиком классов, который использует системный путь к классам. Однако классы, загруженные загрузчиком загрузчиков классов или пользовательскими загрузчиками классов, которые не используют системный путь к классам, не смогут найти класс Recorder . Добавление его в загрузочный путь к классам устраняет эту проблему. Мы можем сделать это, воспользовавшись Instrumentation «s (…) appendToBootstrapClassLoaderSearch метода. Этот метод принимает JarFileв качестве параметра. Это означает, что мы должны связать класс Recorder в его собственный jar и связать этот jar внутри jar агента. Во время инициализации агента мы можем извлечь этот jar- файл и вызвать метод appendToBootstrapClassLoaderSearch (…) . Вот соответствующий код build.xml:

    <target name="jar" depends="compile">

        <jar destfile="${build}/${boot-classpath-jar}" update="true" >
            <fileset dir="${classes}">
                <include name="ca/discotek/agent/example/profileclasspath/Recorder.class"/>
            </fileset>
        </jar>

        <jar destfile="${dist}/${agent-jar}" update="true" >
            <manifest>
                <attribute name="Premain-Class" value="${agent-class-name}"/>
                <attribute name="Agent-Class" value="${agent-class-name}"/>
                <attribute name="Can-Redefine-Classes" value="true"/>
                <attribute name="Can-Retransform-Classes" value="true"/>
            </manifest>

            <fileset dir="${classes}">
                <include name="ca/discotek/agent/example/profileclasspath/asm/*.class"/>
                <include name="ca/discotek/example/rebundled/**/*.class"/>
            </fileset>

            <fileset dir="${build}">
                <include name="${boot-classpath-jar}"/>
            </fileset>

        </jar>

        <jar destfile="${dist}/${test-jar}" update="true" >
            <fileset dir="${test-classes}">
                <include name="ca/discotek/agent/example/profileclasspath/**/*.class"/>
            </fileset>
        </jar>

    </target>

Первая задача jar создает jar с классом Recorder, а вторая задача jar создает jar агента, который объединяет jar Recorder .

Вот метод initialize (…) в классе MyAgent, который извлекает jar- файл и добавляет его в путь загрузки класса:

    public static void initialize(String agentArgs, Instrumentation inst, boolean isPremain) {
        MyAgent.instrumentation = inst;

        try {
            URL url = MyAgent.class.getProtectionDomain().getCodeSource().getLocation();
            File file = new File(url.getFile());
            JarFile agentJar = new JarFile(file);
            ZipEntry entry = agentJar.getEntry("profile-classpath-boot-classpath.jar");
            InputStream is = agentJar.getInputStream(entry);

            File tmpDir = new File(System.getProperty("java.io.tmpdir"));
            File bootClassPathFile = new File(tmpDir, entry.getName());
            FileOutputStream fos = new FileOutputStream(bootClassPathFile);

            int length;
            byte bytes[] = new byte[10 * 1024];
            while ( (length = is.read(bytes)) > 0)
                fos.write(bytes, 0, length);

            fos.close();
            is.close();

            JarFile jar = new JarFile(bootClassPathFile);
            inst.appendToBootstrapClassLoaderSearch(jar);

            inst.addTransformer(new MyAgent(Pattern.compile(agentArgs)), true);
        } 
        catch (Exception e) {
            System.err.println("Unexpected error occured while installing agent. See following stack trace. Aborting.");
            e.printStackTrace();
        }
    }

Вы заметите, что приведенный выше код использует MyAgent.class.getProtectionDomain (). GetCodeSource (). GetLocation () . Это хитрый трюк, но он может не сработать, если вы запустите тестовый код из вашей IDE. Среды IDE, такие как Eclipse, будут пытаться заменить ваш код. Это относится к коду приложения и коду агента. Он выполнит горячую замену класса MyAgent и обнаружит его в каталоге <project> / bin. Если это произойдет, MyAgent.class.getProtectionDomain (). GetCodeSource (). GetLocation () вернет URL-адрес каталога bin , а не агента jar.

Чтобы увидеть этого агента в действии:

  1. Запустите задание по умолчанию ANT-сценария build.xml в корневом каталоге проекта.
  2. Запустите программу ca.discotek.agent.example.profile.asm.test.ProfileAsmTest из оболочки (не из вашей IDE — см. Примечание выше). Вам нужно будет добавить параметр -javaagent . Вот как это может выглядеть:
java -noverify -javaagent:.../discotek-agent-example-8-profile-classpath-asm/dist/agent-profile-classpath-asm.jar=.*discotek.*test.* -classpath .../discotek-agent-example-8-profile-classpath-asm/dist/profile-classpath-test.jar ca.discotek.agent.example.profileclasspath.asm.test.ProfileClasspathAsmTest

На этом завершаются примеры проектов. Я надеюсь, что они просты в использовании и информативны. Чтобы закончить, вот несколько советов для разработки байт-кода:

  • Если вы используете Javassist, вы можете использовать ClassPool.getDefault () для создания объекта ClassPool . Это может стать плохой привычкой. Класс ClassPool по умолчанию не может быть собран сборщиком мусора, и все объекты ClassPool сохранят (по памяти) по крайней мере некоторую часть каждого CtClass, который он загружает. Если вы используете много классов, это может легко привести к утечке памяти. Или создайте новый объект ClassPool и сразу же вызовите appendSystemPath () для достижения аналогичной цели.
  • Если в методе ClassFileTransformer.transform (…) возникает какое-либо исключение , оно, вероятно, будет молча проглочено, и вы не будете иметь представления, почему ваш целевой байт-код не преобразуется. Чтобы избежать этого провала, поместите любой код, который вы поместили в метод transform (…), в блок try / catch , который перехватывает Throwable и обрабатывает исключение таким образом, чтобы сообщить вам, что что-то идет не так.
  • Этот последний совет может не иметь смысла, пока он не случится с вами … когда библиотекам разработки байт-кода, таким как ASM и Javassist, нужно будет найти и проанализировать классы, которые ваш код не знает, как найти. Например, если вы используете инструмент класса X, вы можете получить сообщение об ошибке, в котором говорится, что он не может найти класс Y, который является его суперклассом. Ответ заключается в использовании параметра ClassLoader из метода transform . Полное решение для ASM является довольно сложным и может потребовать перекрывая ClassWriter «s (…) getCommonSuperClass метод. Тем не менее, Javassist немного проще. Вы можете вызвать ClassPool.appendClassPath (…), чтобы добавить LoaderClassPath .

Ресурсы

Все ресурсы, упомянутые в этом руководстве, могут быть загружены со страницы загрузки Практического Байт-кода.