Pull to refresh

Comments 18

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

Оно вероятно нужно в netty подобных либах. Но они от начала до конца попадают в ненормальное программирование.

У всех свой продакшен. Мы вот используем и рады. Да и потом, если не все генерируют байт-код в продакшене, мало ли что. Где-то что-то не срослось и приходится смотреть глазами вывод `javap -c`. Или где-то заглючил спринг или хибернейт и сгенерил какую-нибудь обёртку, не проходящую валидацию. Так что понимать, как устроен байт-код - очень полезный навык, а для этого хорошо поупражняться таким способом.

Кстати, а можете привести конкретные доводы, почему именно "нет ни одного повода"?

Понимать полезно. Не могу не согласиться. Главное чтобы это на собеседованиях спрашивать не начали, блин.

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

Кстати, а можете привести конкретные доводы, почему именно "нет ни одного повода"?

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

Исключения это как раз супероптимизированные джава либы. Netty и подобное. Их на плюсах писать обычно не стоит, а максимум оптимизации нужен. Но сколько людей реально такое писало?

Главное чтобы это на собеседованиях спрашивать не начали, блин.

Проекты проектам рознь. Где-то понимать, как генерируют байт-код JVM - это первейший необходимый навык. В компиляторных командах, например.

Прокси, лямбды и все такое что я перечислил

Прокси, как я уже написал, плохо работают с AOT-компиляторами и proguard/r8. Постоянно приходится думать, а не придётся ли мне после написания или рефакторинга очередной пачки кода завайтлистить ещё каких классов, чтобы приложение не упало. Можно всё то же самое сделать через кодогенерацию. Обычно всё же генерируют исходники через annotation processors, но бывают нюансы.

Ну и кстати, прокси работают только с интерфейсами, а не с абстрактными классами.

Если вам надо лезть в байт код, например для оптимизации, то пора переписывать код на плюсы

Это смотря какого рода оптимизации. Если есть алгоритм, который раз и навсегда написан, то да, это кейс. Если же пользователь что-то вводит и это что-то надо максимально быстро исполнить для разных входных данных (собственно, игрушечный проект в статье - намёк на такой кейс), то я лучше сгенерю байт-код Java, чем напишу на C++ генератор нативного кода. Пример: различные пользовательские скрипты. Да, в 90% случаев проще прикрутить что-то стандартное вроде JS, но бывает всякое. Ну например, представьте, что вы пишете Excel-подобную штуку и пользователь хочет (а он 100% хочет), чтобы формулы пересчитывались максимально быстро.

Плюс JNI имеет достаточно большие накладные расходы. И нативный код не инлайнится.

Проекты проектам рознь. Где-то понимать, как генерируют байт-код JVM - это первейший необходимый навык. В компиляторных командах, например.

Я про обычные места. Компиляторы очень мало кто пишет. Сколько там людей на планете понимает как c2 работает?

Прокси, как я уже написал, плохо работают с AOT-компиляторами и proguard/r8

Тут я пас. javac наше все. Код на сервере.

Ну и кстати, прокси работают только с интерфейсами, а не с абстрактными классами.

С чем угодно они уже довольно работают.

Ну например, представьте, что вы пишете Excel-подобную штуку и пользователь хочет (а он 100% хочет), чтобы формулы пересчитывались максимально быстро.

Возьмите любой готовый движок. SpEl для примера. Не надо писать такое.

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

Плюс JNI имеет достаточно большие накладные расходы. И нативный код не инлайнится.

Уже поменьше. Доработали неплохо. Но да они все равно нормальные такие.

Переписывать лучше всего весь микросервис. Или выделять микросервис где надо упороться в производительность. Это в любом случае полезно. Такие штуки стоит держать отдельно от основного кода.

Сколько там людей на планете понимает как c2 работает

Ну зачем же c2? Я про javac, kotlinc, scalac и иже с ними. c2 - это уже уровнем пониже и немного другие скилы.

Тут я пас. javac наше все. Код на сервере.

Так AOT-компиляторы обычно берут байт-код, там перед AOT-компилятором старый-добрый javac.

С чем угодно они уже довольно работают.

Вы про java.lang.reflect.Proxy? Не поддерживают.

Так AOT-компиляторы обычно берут байт-код, там перед AOT-компилятором старый-добрый javac.

Мои знания кончаются ровно на моменте "байт-код готов". Мне достаточно что он нормально исполняется стандартной джава машиной.

Вы про java.lang.reflect.Proxy? Не поддерживают.

Возьмите стандартный AspectJ или Spring AOP и все у вас будет.

Прокси это довольно широкое понятие.

А у меня на одном из мест работы генерировали классы вполне. И даже в продакшене использовали, на сколько я помню.

у reflection есть один недостаток: он плохо работает с AOT-компиляторами, которым в какой-то мере является, например, Android SDK

Ваш пример будет работать на Android? Насколько я знаю, там другая java-машина, не стековая, а регистровая, и опкоды другие, и бинарный формат классов другой.

Вот конкретно этот пример - не уверен, не пробовал. Если взять из моего примера генерацию байт-кода класса, сохранить его в виде класса с расширением class и прилинковать к проекту - класс загрузится. Какой бы ни был внутри android формат классов, SDK умеет переводить байт-код JVM в свой собственный формат. Не вижу причин, почему бы нельзя было в ClassLoader поддержать прозрачную трансляцию из байт-кода JVM в собственный формат - это должно быть тривиально. Боюсь только, что Android такой сгенерированный на лету байт-код будет интерпретировать или если сконвертирует в нативный код, то с минимальным числом оптимизаций.

Что касается моего личного опыта, то я использую на своём текущем проекте самописные генерялки байт-кода для нужд сериализации данных, RPC и привязки данных к OT. Но работают они в compile time.

Не вижу причин, почему бы нельзя было в ClassLoader поддержать прозрачную трансляцию из байт-кода JVM в собственный формат — это должно быть тривиально

Не совсем тривиально перекинуть стековую машину в регистровую. В compile-time это делает утилита https://developer.android.com/tools/d8


А причина не включать эту возможность в рантайм андроида, я думаю в том, что гугл хотел максимально обезопаситься от оракла по лицензиям и для этого разработал принципиально отличный формат байт-кода. И включать загрузчик классического java-кода — это обесценить все эти усилия.


Но работают они в compile time.

Вот ещё нашёл на Хабре
https://habr.com/en/articles/469237/

В модах к играм это хорошо используется. Например, Rimworld использует harmony, что в целом позволяет переписать байткод во время загрузки(даёт IEnumerable байткода и ждёт то же самое). Forge для Minecraft наверное что-то подобное позволяет.
Что очень удобно: исходников нормальных нет, а шаловливыми ручками изменить нет-нет, да и хочется

Спасибо за статью. ASM и генерация байт-код интересная штука. Мы у себя на работе пишем плагин под Android, который добавляет байт-код инструкции в существующие классы фреймворка, как (fragment, activity), или в различные библиотеки (okhttp, jetpack compose).

Согласен, тестирование и контроль качества не тривиальное дело, но все возможно. Как минимум, можно проверить, что ClassLoader может загрузить измененный класс.

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

Ну и финальный шаг - используем приложение тестовое , применяем плагин и пишем end2end тесты и смотрим поведение.

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

Кстати, для junit 4 можно писать раннеры, которые тестовые классы загружают в кастомном лоадере, что может быть очень удобно при тестировании генераторов. Увы, в 5-м авторы перемудрили с системой расширений и такая возможность пропала.

Самый неоспоримый use case, когда есть некая проприетарная библиотека(как было у нас с Coherence) и надо залазить в ее потроха. Тут и AspectJ, манипуляции с байт кодом, свои загрузчики классов(умеющие подгружать зависимости из maven) и динамическое подключение агента(которое в Java21 уже как раньше не сработает). В таких задачах все средства хороши!

Вставлю свои 5 копеек: Есть довольно экзотические применения, когда java-компилятор не позволяет писать определенные конструкции, в то время как байткод с похожим смыслом может быть успешно загружен в JVM HotSpot. Понятно, что следующий код не может быть скомпилирован

public class Cl{
    private static final int fld;

    public static void setFinalField1(){
        fld = 5;
    }

    public static void setFinalField2(){
        fld = 2;
    }
}

Однако, если нагенерить байткод, который меняет static final поле за пределами static initializer то JVM HotSpot успешно его загрузит. Для этого в коде JVM HotSpot есть специальные проверки cо следующим комментарием (ссылка на исходники):

// Check if any final field of the class given as parameter is modified
// outside of initializer methods of the class. Fields that are modified
// are marked with a flag. For marked fields, the compilers do not perform
// constant folding (as the field can be changed after initialization).
//
// The check is performed after verification and only if verification has
// succeeded. Therefore, the class is guaranteed to be well-formed.

Наткнулся на этот фрагмент несколько лет назад совершенно случайно, когда разбирался в исходниках JVM HotSpot. Зачем это нужно -- для меня так и осталось загадкой.

Менять static final нужно для всяких ленивых инициализаторов.

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

Помню дрючил ASM, чтобы инжектнуть в классы код, проверяющий наличие лицензии для нашего продукта. Причем, вместо вызова сторонней функции инлайнился сам код прямо в методы, рандомно выбранные при сборке. Кроме того, пришлось придумать некое подобие полиморфности при генерации, чтобы код не был вычищен простым поиском по шаблону. Интересно, но на мой взгляд слишком трудозатратно: cамым тяжелым была генерация лямбд.

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

Sign up to leave a comment.

Articles