Знакомимся с Javassist

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Часть первая

Всем большой привет! Перед началом стоит сказать, что библиотека Javassist довольно мощный инструмент, так как стирает почти все границы у того безграничного языка JAVA, позволяя разработчику осуществлять манипуляции связанные с байткодом.

Конечно, получив доступ к байткоду, а ровно и к возможности воздействовать на этот самый байткод вам совсем не обязательно вклиниваться в него. Javassist можно использовать и в “мирных” целях!

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

Итак, если после всех предостережений вы все же решили использовать эту библиотеку, то давайте начинать!

В этой статье мы рассмотрим Javassist, как инструмент, с помощью которого мы будем вклиниваться в существующий байткод и трансформировать его.

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

 Как и говорилось выше, после использования Javassist у вас появится второе полноценное приложение, в котором будет жить Java со своей логикой. Почему это происходит? Почему нельзя запаковать все в одно приложение?

Ответ очевиден – приложение не может само себя изменять. Т.е. приложение не может само изменять свой же байткод. Это должен делать кто-то другой. Этот кто-то другой – такое же Java приложение, но заточенное на работу с байткодом.

Итак, теперь мы знаем, что использование второго приложения, в котором и будет крутиться вся логика, связанная с использованием Javassist просто неизбежно. Дело в том, что это самое приложение загружается в JVM первым, разворачивается там и начинает пропускать через себя все классы, которые необходимы для работы уже самого целевого приложения.

Что же происходит под капотом JVM? Каким образом первое приложение с Javassist может пропускать через себя байткод? Как это вообще работает?

Все мы привыкли видеть в Maven такой тег как <manifestFile>. В этом теге указывается путь до файла MANIFEST.MF. В свою очередь в файле манифеста прописывается точка входа в приложение. Всегда это выглядит так: Main-Class: путь до класса.Класс в котором расположен одноименный метод main. Но Javassist в силу своей специфики априори не может запускаться как обычное Java-приложение. Должно быть что-то, что отличает такое “чудо-приложение” от обычного. И, конечно, такая особенность есть. Дело в том, что в приложение Javassist нет привычного для нас всех метода main. Вместо этого метода используется метод, который именуется как premain. В принципе, название говорит само за себя. Этот метод главнее чем метод main. Собственно, потому он и называется premain. Логично, что и содержимое файла манифеста MANIFEST.MF теперь будет другим. Вместо “Main-Class” теперь будет использоваться “Premain-Class:”.

Содержимое MANIFEST.MF с Premain-Class:

Manifest-Version: 1.0
Premain-Class: app.Agent
Built-By: Vasilyev Pavel
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_201

Листинг 1.

На вкус и цвет…, но все же не несущие особого value параметры я удалю:

Premain-Class: app.Agent

Листинг 2.

Так будет выглядеть MANIFEST.MF, если мы все это будем прописывать руками. Этот манифест в дальнейшем будет зашит в JAR файл и лежать вместе с package, в которых находятся скомпилированные Java-классы. Чтобы избежать лишнего конфигурирования мы воспользуемся плагином, который сам все упакует и подложит куда нужно.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.4</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <finalName>agent</finalName>
                <transformers>
                    <transformer
                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <manifestEntries>
                            <Premain-Class>app.Agent</Premain-Class>
                        </manifestEntries>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

Листинг 3.

И конечно же мы не можем забыть, собственно, о самой библиотеке Javassist.

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.20.0-GA</version>
</dependency>

Листинг 4.

В итоге мы имеем Maven-проект с подключенной библиотекой в зависимостях pom.xml.

Весь код pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.agent</groupId>
    <artifactId>javaagent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven-compiler-plugin.source.version>1.8</maven-compiler-plugin.source.version>
        <maven-compiler-plugin.target.version>1.8</maven-compiler-plugin.target.version>
        <maven-compiler-plugin.inherited>true</maven-compiler-plugin.inherited>
        <maven-compiler-plugin.encoding.version>UTF-8</maven-compiler-plugin.encoding.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.20.0-GA</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <scope>compile</scope>
            <version>3.11</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>agent</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>app.Agent</Premain-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Листинг 5.

Теперь, когда все готово для упаковки в нужную структуру, можно написать и сам код, который будет исполняться и пропускать через себя байткод скомпилированных классов из другого JAR файла. Но так как мы еще не написали Java-код для целевого Java-проекта, то оставим эту заготовку в виде Maven проекта с подключенной библиотекой, а потом дополним эту заготовку логикой по трансформации исполняемого байткода.

Пишем целевое Java-приложение

Это будет самый обычный java-проект, в котором существует класс Main:

public class Main {

    static int myInt1 = 77;

    public static void main(String[] args) {
        System.out.println("Hello World and myInt1 = " + myInt);
    }
}

Листинг 6.

Наша цель:

Внедрить в байткод класса Main свою реализацию метода main и запустить код на исполнение. Итак, наше целевое приложение для опытов написано! Теперь соберем его в Jar архив и приступим к наполнению логикой нашей заготовки, которую мы бережно написали в первой части.

Работаем с Javassist

Ранее мы подготовили структуру проекта и подключили необходимые зависимости.

Напишем саму логику внедрения в байткод. Для этого создадим класс “Agent” и добавим в него единственный метод “premain”. Данный метод абсолютно такой же как метод “main” в простом классическом приложении типа “Hello World” с той лишь разницей, что метод “main” теперь – “premain” и этот метод принимает на вход два параметра. Хоть разница и не значительная но после применения приставки “pre”, наше приложение перестает быть классическим и его уже не получится так просто собрать в два клика через Intellij Idea.

public static void premain(String agentArgs, Instrumentation instrumentation) {
}

Листинг 7.

Собрать такой проект можно либо руками, компилируя каждый класс, создавая манифест файл, а потом запаковывать все в Jar файл, либо создать Maven проект и автоматизировать всю сборку при помощи определенных “plugin”. Конечно же мы выберем второй вариант, через Maven, так как нам нужно подключить еще и саму библиотеку Javassist.

 Здесь-то нам и пригодится наш подготовленный конфиг файла pom.xml (см.выше).

 Главный метод “premain” создан и теперь нужно написать соответствующую логику по трансформации байткода целевого приложения. Для этого воспользуемся ”Instrumentation”, ссылку на который мы получили в параметрах. Только перед тем, как использовать ”Instrumentation” мы должны добавить класс, в котором будет описана логика по трансформации загружаемого байткода из классов.

Вот сам класс, в котором происходит трансформация байткода (ниже будет пояснение):

public class ClassTransformer implements ClassFileTransformer {

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

        byte[] byteCode = classfileBuffer;

        if ("com.company.Main".equals(className.replaceAll("/", "."))) {

            try {
                ClassPool pool = ClassPool.getDefault();
                CtClass ctClass = pool.get("com.company.Main");
                CtMethod myMain = ctClass.getDeclaredMethod("main");
                ctClass.removeMethod(myMain);

                CtField toBeDeleted = ctClass.getField("myInt1");
                ctClass.removeField(toBeDeleted);
                CtField ctField = new CtField(CtClass.intType, "myInt1", ctClass);
                ctField.setModifiers(Modifier.STATIC | Modifier.FINAL | Modifier.PUBLIC);
                ctClass.addField(ctField, "123");

                CtField name = CtField.make("static int myInt2 = 45;", ctClass);
                ctClass.addField(name);

                ctClass.addMethod(CtNewMethod.make("public static void main(String[] args) { int localInt = 67; System.out.println(\"Our numbers : \" + myInt1 + \" : \" + myInt2 + \" : \" + localInt);}", ctClass));
                ctClass.addMethod(CtNewMethod.make("public void onEvent(){System.out.println(\"Hello World\");}", ctClass));

                CtMethod[] methods = ctClass.getDeclaredMethods();

                for (CtMethod method : methods) {
                    System.out.println("!!!!!!! + " + method.getName());
                    if (method.getName().equals("main")) {
                        try {
                            method.insertAfter("System.out.println(\"Logging using Agent\");");
                        } catch (CannotCompileException e) {
                            e.printStackTrace();
                        }
                    }
                }
                try {
                    byteCode = ctClass.toBytecode();
                    ctClass.detach();
                    return byteCode;
                } catch (IOException e) {
                    e.printStackTrace();
                }
                ctClass.detach();
                return byteCode;
            } catch (NotFoundException e) {
                System.out.println(e.getMessage());
            } catch (CannotCompileException e) {
                e.printStackTrace();
            }

        }
        return byteCode;
    }
}

Листинг 8.

А теперь добавим это класс в качестве аргумента для метода addTransformer.

instrumentation.addTransformer(new ClassTransformer());

Листинг 9.

Ну, все готово для запуска и проверки трансформации нашего байткода.

 Перед проверкой мы конечно же разберем что происходит в переопределенном методе “transform” (Листинг 8).

Самое первое на что мы обращаем внимание, так это на то, что данный метод возвращает массив байтов. Дело в том, что на вход данного метода одним из параметров подается массив байт от класса, который загружается. Таким образом в метод “transform” попадают абсолютно все классы, которые загружаются в JVM. Точнее не сами классы, а байткод классов. Именно в тот момент, когда в наш метод “transform” попал байткод определенного класса мы можем трансформировать его на свой лад и вернуть байткод, но в уже в “редактированном виде”. Такое “редактирование” называется “трансформация”.

 И сразу же, что приходит в голову, так это то, как мы редактируем байткод, ведь он представлен в шестнадцатеричной системе? Конечно же, разбираться в последовательности закодированных значений довольно не простая задача, да и нам это ни к чему. Ведь специально для таких вот случаев и была написана библиотека Javassist!

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

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

Начинается самое интересное. Помните, в листинге 6, мы написали нашу стандартную программу из разряда “Hello World”? В нашем варианте мы усовершенствовали “Hello World” и к одноименной фразе добавили еще вывод числа “myInt1”. Теперь наша программа выводит в консоль фразу "Hello World and myInt1 = 77".

Попробуем “трансформировать” байткод класса Main, а именно изменить вывод данной строки, да не просто изменить, а еще попытаемся определить новые переменные, присвоить им значения и вывести все это в консоль!

В листинге 8, видно, что общаться с кодом, через библиотеку Javassist довольно просто. Первое что мы должны сделать – остановиться на том классе, в байткод которого требуется вмешательство. Поэтому обращаемся к переменной “classname” и проверяем на нужном ли классе мы находимся. Если на нужном, то объект, который будет подвержен трансформации найден и можно его начать изменять.

Думаю, что дальше коментарии будут лишними, так как в коде и так все понятно.

Итак, осталось сделать последнее действие – запустить оба наших jar-архива! Это можно сделать командой:

java -javaagent:agent.jar -jar demo.jar

Следует отметить, что запускаемый javaagent – обычная java-программа, а следовательно при запуске можно прописывать все ключи, которые могут быть вам необходимы.

После трансформации наш вывод будет иметь следующий вид:

Our numbers : 123 : 45 : 67

Summary:

Наверное на этом стоит остановиться, потому что чем хотел поделиться я поделился:

- мы научились создавать java-проект, который загружается первым и трансформирует код другого загружаемого проекта, путем пропускания его байткода через себя;

- мы научились писать код для трансформации загружаемого байткода;

- мы научились запускать последовательность созданных jar-файлов javaagent и целевого приложения.

 Надеюсь эта статья помогла тем, кто хочет начать изучение библиотеки Javassist.

Если есть какие-то дополнения и комментарии, то пожалуйста пишите. Буду рад любой обратной связи. Так же прошу поделиться своими знаниями по тонкостям использования данной библиотеки.

Всем большое спасибо и больших успехов!

Источник: https://habr.com/ru/post/664818/


Интересные статьи

Интересные статьи

Недавно на проекте интегрировал модуль CRM Битрикса c виртуальной АТС Ростелеком. Делал по стандартной инструкции, где пошагово показано, какие поля заполнять. Оказалось, следование ей не гаран...
На работе я занимаюсь поддержкой пользователей и обслуживанием коробочной версии CRM Битрикс24, в том числе и написанием бизнес-процессов. Нужно отметить, что на самом деле я не «чист...
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
В интернет-магазинах, в том числе сделанных на готовых решениях 1C-Битрикс, часто неправильно реализован функционал быстрого заказа «Купить в 1 клик».
Эта статья для тех, кто собирается открыть интернет-магазин, но еще рассматривает варианты и думает по какому пути пойти, заказать разработку магазина в студии, у фрилансера или выбрать облачный серви...