Русский
Русский
English
Статистика
Реклама

Reflection

Тривиальная и неправильная облачная компиляция

28.01.2021 00:21:03 | Автор: admin


Введение


Данная статья не история успеха, а скорее руководство как не надо делать. Весной 2020 для поддержания спортивного тонуса участвовал в студенческом хакатоне (спойлер: заняли 2-е место). Удивительно, но задача из полуфинала оказалась более интересной и сложной чем финальная. Как вы поняли, о ней и своём решении расскажу под катом.


Задача


Данный кейс был предложен Deutsche Bank в направлении WEB-разработка.
Необходимо было разработать онлайн-редактор для проекта Алгосимулятор тестового стенда для проверки работы алгоритмов электронной торговли на языке Java. Каждый алгоритм реализуется в виде наследника класса AbstractTradingAlgorythm.


AbstractTradingAlgorythm.java
public abstract class AbstractTradingAlgorithm {    abstract void handleTicker(Ticker ticker) throws Exception;    public void receiveTick(String tick) throws Exception {        handleTicker(Ticker.parse(tick));    }    static class Ticker {        String pair;        double price;       static Ticker parse(String tick) {           Ticker ticker = new Ticker();           String[] tickerSplit = tick.split(",");           ticker.pair = tickerSplit[0];           ticker.price = Double.valueOf(tickerSplit[1]);           return ticker;       }    }}

Сам же редактор во время работы говорит тебе три вещи:


  1. Наследуешь ли ты правильный класс
  2. Будут ли ошибки на этапе компиляции
  3. Успешен ли тестовый прогон алгоритма. В данном случае подразумевается, что "В результате вызова new <ClassName>().receiveTick(RUBHGD,100.1) отсутствуют runtime exceptions".


Ну окей, скелет веб-сервиса через spring накидать дело на 5-10 минут. Пункт 1 работа для регулярных выражений, поэтому даже думать об этом сейчас не буду. Для пункта 2 можно конечно написать синтаксический анализатор, но зачем, когда это уже сделали за меня. Может и пункт 3 получится сделать, использовав наработки по пункту 2. В общем, дело за малым, уместить в один метод, ну например, компиляцию исходного кода программы на Java, переданного в контроллер строкой.


Решение


Здесь и начинается самое интересное. Забегая вперёд, как сделали другие ребята: установили на машину джаву, отдавали команды на ось и грепали stdout. Конечно, это более универсальный метод, но во-первых, нам сказали слово Java, а во-вторых...



у каждого свой путь.


Естественно, Java окружение устанавливать и настраивать всё же придётся. Правда компилировать и исполнять код мы будем не в терминале, а, как бы это ни звучало, в коде. Начиная с 6 версии, в Java SE присутствует пакет javax.tools, добавленный в стандартный API для компиляции исходного кода Java.
Теперь привычные понятия такие, как файлы с исходным кодом, параметры компилятора, каталоги с выходными файлами, сообщения компилятора, превратились в абстракции, используемые при работе с интерфейсом JavaCompiler, через реализации которого ведётся основная работа с задачами компиляции. Подробней о нём можно прочитать в официальной документации. Главное, что оттуда сейчас перейдёт моментально в текст статьи, это класс JavaSourceFromString. Дело в том, что, по умолчанию, исходный код загружается из файловой системы. В нашем же случае исходный код будет приходить строкой извне.


JavaSourceFromString.java
import javax.tools.SimpleJavaFileObject;import java.net.URI;public class JavaSourceFromString extends SimpleJavaFileObject {    final String code;    public JavaSourceFromString(String name, String code) {        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);        this.code = code;    }    @Override    public CharSequence getCharContent(boolean ignoreEncodingErrors) {        return code;    }}

Далее, в принципе уже ничего сложного нет. Получаем строку, имя класса и преобразуем их в объект JavaFileObject. Этот объект передаём в компилятор, компилируем и собираем вывод, который и возвращаем на клиент.
Сделаем класс Validator, в котором инкапсулируем процесс компиляции и тестового прогона некоторого исходника.


public class Validator {    private JavaSourceFromString sourceObject;    public Validator(String className, String source) {        sourceObject = new JavaSourceFromString(className, source);    }}

Далее добавим компиляцию.


public class Validator {    ...    public List<Diagnostic<? extends JavaFileObject>> compile() {        // получаем компилятор, установленный в системе        var compiler = ToolProvider.getSystemJavaCompiler();        // компилируем        var compilationUnits = Collections.singletonList(sourceObject);        var diagnostics = new DiagnosticCollector<JavaFileObject>();        compiler.getTask(null, null, diagnostics, null, null, compilationUnits).call();        // возворащаем диагностику        return diagnostics.getDiagnostics();    }}

Пользоваться этим можно как-то так.


public void MyMethod() {        var className = "TradeAlgo";        var sourceString = "public class TradeAlgo extends AbstractTradingAlgorithm{\n" +                "@Override\n" +                "    void handleTicker(Ticker ticker) throws Exception {\n" +                "       System.out.println(\"TradeAlgo::handleTicker\");\n" +                "    }\n" +                "}\n";        var validator = new Validator(className, sourceString);        for (var message : validator.compile()) {            System.out.println(message);        }    }

При этом, если компиляция прошла успешно, то возвращённый методом compile список будет пуст. Что интересно? А вот что.

На приведённом изображении вы можете видеть директорию проекта после завершения выполнения программы, во время выполнения которой была осуществлена компиляция. Красным прямоугольником обведены .class файлы, сгенерированные компилятором. Куда их девать, и как это чистить, не знаю жду в комментариях. Но что это значит? Что скомпилированные классы присоединяются в runtime, и там их можно использовать. А значит, следующий пункт задачи решается тривиально с помощью средств рефлексии.
Создадим вспомогательный POJO для хранения результата прогона.


TestResult.java
public class TestResult {    private boolean success;    private String comment;    public TestResult(boolean success, String comment) {        this.success = success;        this.comment = comment;    }    public boolean success() {        return success;    }    public String getComment() {        return comment;    }}

Теперь модифицируем класс Validator с учётом новых обстоятельств.


public class Validator {    ...    private String className;    private boolean compiled = false;    public Validator(String className, String source) {        this.className = className;        ...    }    ...    public TestResult testRun(String arg) {        var result = new TestResult(false, "Failed to compile");        if (compiled) {            try {                // загружаем класс                var classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI().toURL()});                var c = Class.forName(className, true, classLoader);                // создаём объект класса                var constructor = c.getConstructor();                var instance = constructor.newInstance();                // выполняем целевой метод                c.getDeclaredMethod("receiveTick", String.class).invoke(instance, arg);                result = new TestResult(true, "Success");            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException | RuntimeException | MalformedURLException | InstantiationException e) {                var sw = new StringWriter();                e.printStackTrace(new PrintWriter(sw));                result = new TestResult(false, sw.toString());            }        }        return result;    }}

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


public void MyMethod() {        ...        var result = validator.testRun("RUBHGD,100.1");        System.out.println(result.success() + " " + result.getComment());    }

Вставить этот код в реализацию API контроллера задача нетрудная, поэтому подробности её решения можно опустить.


Какие проблемы?


  1. Ещё раз напомню про кучу .class файлов.


  2. Поскольку опять же идёт работа с компиляцией некоторых классов, есть риск отказа в записи любого непредвиденного .class файла.


  3. Самое главное, наличие уязвимости для инъекции вредосного кода на языке программирования Java. Ведь, на самом деле, пользователь может написать что угодно в теле вызываемого метода. Это может быть вызов полного перегруза системы, затирание всей файловой системы машины и тому подобное. В общем, исполняемую среду нужно изолировать, как минимум, а в рамках хакатона этим в команде никто естественно не занимался ввиду нехватки времени.



Поэтому делать в точности как я не надо)


P.S. Ссылка на гитхаб с исходным кодом из статьи.

Подробнее..

Как определить размер переменных во время выполнения Go-программы

27.06.2020 22:14:45 | Автор: admin
Аннотация: в заметке рассматривается один из способов анализа потребления памяти компонентами Go-приложения.
Зачастую в памяти программы хранятся структуры данных, которые изменяют свой размер динамически, по ходу работы программы. Примером такой структуры может быть кэш данных или журнал работы программы или данные, получаемые от внешних систем. При этом может возникнуть ситуация, когда потребление памяти растёт, возможностей оборудования не хватает, а конкретный механизм утечки не ясен.
Основным способом профилирования Go-приложений является подключение инструмента pprof из пакета net/http/pprof. В результате можно получить таблицу или граф с распределением памяти в работающей программе. Но использование этого инструмента требует очень больших накладных расходов и может быть неприменимо, особенно если вы не можете запустить несколько экземпляров программы с реальными данными.
В таком случае возникает желание измерить потребление памяти объектами программы по запросу, чтобы, например, отобразить статистику системы или передать метрики в систему мониторинга. Однако средствами языка это в общем случае невозможно. В Go нет инструментов для определения размера переменных во время работы программы.
Поэтому я решил написать небольшой пакет, который предоставляет такую возможность. Основным инструментом является рефлексия (пакет reflection). Всех интересующихся вопросом такого профилирования приложения приглашаю к дальнейшему чтению.


Сначала нужно сказать пару слов по поводу встроенных функций
unsafe.Sizeof(value)
и
reflect.TypeOf(value).Size()

Эти функции эквивалентны и зачастую в Интернете именно их рекомендуют для определения размера переменных. Но эти функции возвращают не размер фактической переменной, а размер в байтах для контейнера переменной (грубо размер указателя). К примеру, для переменной типа int64 эти функции вернут корректный результат, поскольку переменная данного типа содержит фактическое значение, а не ссылку на него. Но для типов данных, содержащих в себе указатель на фактическое значение, вроде слайса или строки, эти функции вернут одинаковое для всех переменных данного типа значение. Это значение соответствует размеру контейнера, содержащего ссылку на данные переменной. Проиллюстрирую примером:
func main() {s1 := "ABC"s2 := "ABCDEF"arr1 := []int{1, 2}arr2 := []int{1, 2, 3, 4, 5, 6}fmt.Printf("Var: %s, Size: %v\n", s1, unsafe.Sizeof(s1))fmt.Printf("Var: %s, Size: %v\n", s2, unsafe.Sizeof(s2))fmt.Printf("Var: %v, Size: %v\n", arr1, reflect.TypeOf(arr1).Size())fmt.Printf("Var: %v, Size: %v\n", arr2, reflect.TypeOf(arr2).Size())}

В результате получим:
Var: ABC, Size: 16Var: ABCDEF, Size: 16Var: [1 2], Size: 24Var: [1 2 3 4 5 6], Size: 24

Как видите, фактический размер переменной не вычисляется.
В стандартной библиотеке есть функция binary.Size() которая возвращает размер переменной в байтах, но только для типов фиксированного размера. То есть если в полях вашей структуры встретится строка, слайс, ассоциативный массив или просто int, то функция не применима. Однако именно эту функция я взял за основу пакета size, в котором попытался расширить возможности приведённого выше механизма на типы данных без фиксированного размера.
Для определения размера объекта во время работы программы необходимо понять его тип, вместе с типами всех вложенных объектов, если это структура. Итоговая структура, которую необходимо анализировать, в общем случае представляется в виде дерева. Поэтому для определения размера сложных типов данных нужно использовать рекурсию.
Таким образом вычисление объёма потребляемой памяти для произвольного объекта представляется следующим образом:
  • алгоритм определение размера переменной простого (не составного) типа;
  • рекурсивный вызов алгоритма для элементов массивов, полей структур, ключей и значений ассоциативных массивов;
  • определение бесконечных циклов;

Чтобы определить фактический размер переменной простого типа (не массива или структуры), можно использовать приведённую выше функцию Size() из пакета reflection. Эта функция корректно работает для переменных, содержащих фактическое значение. Для переменных, являющихся массивами, строками, т.е. содержащих ссылки на значение нужно пройтись по элементам или полям и вычислить значение каждого элемента.
Для анализа типа и значения переменной пакет reflection упаковывает переменную в пустой интерфейс (interface{}). В Go пустой интерфейс может содержать любой объект. Кроме того, интерфейс в Go представлен контейнером, содержащим два поля: тип фактического значения и ссылку на фактическое значение.
Именно отображение анализируемого значения в пустой интерфейс и обратно послужило основанием для названия самого приёма reflection.
Для лучшего понимания работы рефлексии в Go рекомендую статью Роба Пайка в официальном блоге Go. Перевод этой статьи был на Хабре.
В конечном итоге был разработан пакет size, который можно использовать в своих программах следующим образом:
package mainimport ("fmt""github.com/DmitriyVTitov/size")func main() {a := struct {a intb stringc boold int32e []bytef [3]int64}{a: 10,                    // 8 bytesb: "Text",                // 4 bytesc: true,                  // 1 byted: 25,                    // 4 bytese: []byte{'c', 'd', 'e'}, // 3 bytesf: [3]int64{1, 2, 3},     // 24 bytes}fmt.Println(size.Of(a))}// Output: 44

Замечания:
  • На практике вычисление размера структур объёма около 10 ГБайт с большой вложенностью занимает 10-20 минут. Это результат того, что рефлексия довольно дорогая операция, требующая упаковки каждой переменной в пустой интерфейс и последующий анализ (см. статью по ссылке выше).
  • В результате сравнительно невысокой скорости, пакет следует использовать для примерного определения размера переменных, поскольку в реальной системе за время анализа большой структуры фактические данные наверняка успеют измениться. Либо обеспечивайте исключительный доступ к данным на время расчёта с помощью мьютекса, если это допустимо.
  • Программа не учитывает размер контейнеров для массивов, интефейсов и ассоциативных массивов (это 24 байта для массива и слайса, 8 байт для map и interface). Поэтому, если у вас большое количество таких элементов небольшого размера, то потери будут существенными.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru