Как мы мигрировали на Scalar

Некоторое время назад, решили мы поменять тошнотворный Swagger на что-то современное. Остановились на Scalar-е. Современный, компактный и возможностей побольше. Здесь постараюсь рассказать с чем столкнулись при этом.

Действующие лица

  1. Текущий проект. На котором API документация показывается в Swagger-е
  2. OpenAPI. Спецификация по которой генеруется структура и описание REST API. Собственно то во что развился Swagger.
  3. Swagger. В нашем случае UI, который отображает данные сгенерированные по стандарту OpenAPI.
  4. Scalar. Просто UI, который отображает данные сгенерированные по стандарту OpenAPI и на который мы хотим смигрировать Swagger.
  5. Springdoc. Прослойка между SpringBoot и нашей документацией

Инициируем типа наш текущий проект со Swagger-ом

  1. Идем на сайт Spring, забираем SpringBoot3 приложение и добавляем в IDE
  2. Исправляем зависимость:

    на
  3. Создаём котроллер, который что-то возвращает:
  4.  Запускаем приложение и идем по адресу localhost:8080/demo. Видим результат:

    У нас есть приложение.

Получаем OpenAPI спецификацию

Здесь как раз нам помогает Springdoc:

  1. Добавляем зависимость:
  2. Добавляем в application.yaml
  3. Идем по адресу localhost:8080/demo. Видим результат:

    Как можно заметить, мы получили в ответ JSON файл, который собственно и содержит всю документацию о моем эндпоинте. Теперь его можно скармливать в любой UI или клиент. Они сами будут его парсить и отображать.

Swagger

  1. Добавляем зависимость:
  2. Идем по адресу localhost:8080/swagger-ui. Видим результат:Ностальгируем по 2015 году.

Вот в таком состоянии была наша документация перед миграцией на Scalar.

Первый подход. Дёшево, надежно, практично

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

  1. Добавляем конфигурацию:

    Как видно эндпоинты мы группируем по пакетам
  2. Идем поадресу localhost:8080/swagger-ui. Получаем:

Как можно заметить, получился комбобоксик в правом верхнем углу, где можно переключаться между разными группами эндпоинтов. По началу это меня не устраивало, поскольку невозможно было разделить по ролям для отображения. Для этого нужно было иметь два разных эндпоинта, которые можно было бы подключать к соответствующим OpenAPI ссылкам.

Решение с мигрцией на Scalar, которое нашлось в сети, получилось весьма простым, топорным и очень гибким. Нужно просто на своей стороне генерировать HTML код:

  1. Убираем зависимость:
  2. Создаём свой собственный Scalar контроллер:

    Как это работает. Браузер получает HTML, загружает JavaScrip в строке 28, который в свою очередь получает конфигруцию из строки 27 и отрисовывает UI. А в строке 27 хранится пока путь к соответствующей OpenAPI документации.

    Также есть возможность модифицировать HTML. Например, задать нужное название вкладки.

  3. Идём по соотвествующим ссылкам. Видим необходимые документации:

Вуаля! Всё работает.

Но. В процессе код-ревью выясняется, что мы не хотим генерировать HTML и не хотим прятать документацию по ролям (т.е. все должны видеть всё). Начитается поиск иного пути.

Современный программист больше конфигуратор

Правильно. Меньше кода – больше конфигурации. Приходится открывать документацию по Scalar-у и читать ((

Итак, делаем как написано в доке:

  1. Добавляем зависимость:
  2. Добавляем свойства:
  3. Идём по адресу localhost:8080/docs И пара-пара-пам! Приплыли! Сливаем воду! Иду на https://mvnrepository.com/artifact/com.scalar.maven/scalar. Вижу последнюю версию 0.4.4 (версии 0.1.0 нет вообще!). Пробую 0.4.4 – без изменений. Оригинальная документация бесполезна!

Конфигуратор не сдаётся никогда!

Ещё варианты? Кто-то где-то слышал, что Springdoc поддерживает Scalar. Удивительно но факт. Начинаю подключать Scalar через Springdoc:

  1. Удаляем зависимость, что добавляли прежде:
  2. Добавляем зависимость:
  3. Также давайте не забывать, что у нас уже был подключён Swagger. Поэтому нужно вернуть часть от Swagger-а назад дабы вернуть состояние приложения с которым я работал:

  4. Запускаем, идём на localhost:8080/docs. И, о, чудо! Заработало!Только есть проблема. Работает как Swagger, так и Scalar. Вроде решение очевидное. Давайте изменим свойство springdoc.swagger-ui.enabled=false. Делаем. И не работает! Ни Swagger, ни Scalar! С ума сойти! Нафига мне две UI-ки? Лезу в код:

И что мы видим? А видим мы прямую зависимость Scalar-а от Swagger-а (стр. 24-25). Зачем?!

Как позже выяснилось отсюда было два выхода. Убрать зависимость Swagger-а или обновить версию Springdoc-а. Я пошёл по второму пути. Версия 2.8.15 решила эту проблему:

Супер! Анализируем, что получилось!

Основной навык конфигуратора отгадайка

Итак у нас получился весьма симпатичный результат:

Комбобоксик слева вверху. Франкенштейн в конфигурации. Там Springdoc использует конфигурацию Scalar-а. Я промочу о логотипчике и других конфигурационных свойствах, которые собирались отовсюду. Вроде всё устраивает, кроме названий селекторов в комбобоксике. Логично предположить, что это title в настройках скалара. Ок. Делаю:

И что? И ничего. Всё без изменений! Мелочь, казалось бы. Но это нужно исправлять! А как понять, что делать? Опять таки, где читать? Нигде. Остаётся код. Лезем в код опять:

И что мы видим? А видим мы, мать вашу, тот же самый контроллер, который я написал вначале. Ничего сложного! Но вот, что касается нужного кода, то его собственно нет. Уходит он в метод org.springdoc.scalar.AbstractScalarController::getDocs. Только вот проблема. Нет такого класса в этом джарнике. Нет даже такого пакета! Хотя пакет от Springdoc!

Запускаю поиск класса AbstractScalarController в Jar Explorer-e по всему локальному репозиторию. И нахожу:

Сука! В common-е! В common-е лежит класс отвечающий за отображение Scalar-а! Зачем?!

Ладно, идём в код:

А в коде мы видим, что source инициируется особым образом в строке 39. title берется не из свойств Scalar-a, а из свойств групп, которые мы определяли ранее в Java коде! Причём, два дурих свойств Scalar-a тупо обнуляются! То есть их невозможно установить от слова никогда! Также рекомендую обратить внимание на строчки 42 и 43. Сейчас нам они не понадобятся, но в дальнешем, еще с ними столкнёмся в другой проблеме. Там тупо меняется композиционная переменная контроллера т.е. синглтона.

Ок. Довавляем названия в группы:

Смотрим, что получилось:

Супер! Что и требовалось!

  • мы мигрировали на Scalar
  • вся миграция в виде конфигурации
  • эндпоинты разбиты на группы
  • также был добавлен логотипчик, убрана консоль разработчика, добавлен title на табку браузера и другие мелочи

Вроде всё готово! Можно в прод!

Деплою в темп для теста и… нихрена не работает!

Страничка грузится, JavaScript загружается, всё подтягивается кроме OpenAPI JSON файла. Ссылка на него: “url”:”http://temp:443/v3/api-docs/demo-group”. Кик видим протокол HTTP, а порт HTTPS. Браузер такое наебалово не любит, а поэтому отказывается загружать.

Пробую как-то это исправить на своей стороне. Документация бесполезна, поэтому сразу лезу в код:

Что бросается в глаза? Строчки с 3 по 6. Самый главный вопрос зачем? Зачем тянуть в код тестовый URL Scalar-a? Это когда вы строите демку Scalar-a, чтобы посмотреть как работает. Но зачем её протягивать сквозь Java код – непонятно севершенно!

Тем не менее, вроде как, можно в Scalar-е указать один верхний URL типа: /v3/api-docs и всё должно подтянуть (условие не сработает). Делаю. И всё работает! Ура! Но только первый раз при заходе на страничку. Дальше – нифига! А почему? А помните те две строчки выше, на которые я обращал внимание? Вот они то и влияют на это поведение. Первый раз все хорошо, а потом null вместо URL потому как оно перетёрлось в свойствах.

Следующим действием попробовал я последнюю версию org.springdoc:springdoc-openapi-starter-webmvc-scalar. На тот момент 3.0.1. А эта зараза требует SpringBoot4. Я вроде подглядел в код последнего Springdoc Scalar-a и мне показалось что там что-то изменено. Посим я из последних сих заодно обновил наш SpringBoot3 со всеми зависимостями, но всё было по прежнему!

Выть хотелось! Бред какой-то. Никак здесь в моём случае невозможно избежать абсолютной ссылки.

Как выяснилось позже наша аппка была спрятана за проксиком, а эти проксики на каждом энвайрменте были по разному сконфигурированы, на одном, например, был протокол HTTP, а на другом HTTPS. Поэтому пришлось захардкодить в настройках деплоймента соответствующие хедеры:

После этого всё заработало.

P.S. Польза от всякоразных ИИ закончилась на кастомном контроллере. В дальнейшем все, что мне удалось попробовать, были абсолютно бесполезны. Один из них до последнего пытался мне вставить свойство логотипчика в конфигурацию Scalar-а. Хотя его там никогда не было и нет до сих пор. Такое…

Дефолтные методы

Нужно где-то положить конец этому вопросу. Его раньше любили на собеседованиях. Да и для программиста не лишним будет окончательное понимание как оно работает.

Итак, в Java8 появились дефолтные методы в интерфейсах. Появились как костыль, на котором удержалось backward compatibility. Например, как имплементировать следующий код не положив код написанный в ранних версиях Java?

Ведь если мы добавим метод в интерфейс List, то предыдущие версии попадают с ошибкой, поскольку такого метода нет в их имплементации наследников этого интерфейса. А если я скажу что метод stream() добавлен ещё выше? В интерфейсе Collection. А forEach() в Iterable? Ляжет ВСЁ! Можно было бы создать отдельный утилитный класс типа Collections, который бы предоставлял нужные обёртки. Но выглядело это как некий оверхэд. Поэтому были придуманы дефолтные методы для интерфейсов. Ну и туда же положили статические методы.

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

Static

Начнем с простейшего. Со статических методов.

Предлагаю рассмотреть следующий код:

Есть два интерфейса с одним и тем же статическим методом. И класс, который имплементит эти два интерфейса. В данном случае Java не обязывает имплементить статический метод интерфейса. Мы можем добавить такой же статический метод в класс С и он никакого отношения не будет иметь к методам родительских интерфейсов. Собственно, работает так же само как и для обычных классов. Если мы попытаемся добавить аннотацию @Override к методу С.stat() получим ошибку: “The method stat() of type C must override or implement a supertype method”.

То есть статические методы можно использовать смело не боясь конфликтов в имплементациях.

Abstract methods

Рассмотрим следующий отрывок кода:

Всё как в предыдущем примере. Два одинаковых метода. Класс замечательно переопределяет этот метод. Кажется, ничего сложного нет и кейс проще, чем со статикой. Но давайте изменим возвращаемый тип метода в интерфейсе А. Получим ошибку при компиляции класса С: “The return type is incompatible with A.apply()”. Также интересная ситуация будет с исключением, если его нужно объявить в классе С. В таком случае это же исключение должно быть добавлено ко всем методам родительских интерфейсов. С другой стороны всё проще. Исключения могут быть объявлены в интерфейсах какие угодно и сколько угодно. Переопределяемый метод в классе может их полностью игноировать.

Получается, что абстрактные методы с одинаковой сигнатурой могут быть переопределны в классе только в том случае, если у них одинаковый возвращаемый тип и выбрасываемое исключение “покрывается” (поглощается родительским типом исключения или указано в родительском интерфейсе) всеми родительскими интерфейсами.

Default methods

Ну и наконец дефолтные методы:

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

Мой первый опыт работы с Kotlin

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

Ну и одновременно с этим стояла задача прокачать скилы чтоб типа мощно, модно, молодёжно. Поэтому получился следующий стэк: Angular 16.0.0, SpringBoot 3.0.6, Java 17 ну и Kotlin. Про недостатки ангуляра и спринга здесь опущу и сфокусируюсь на моих эмоциях по поводу последнего.

Услышал я о нём давно. Уж точно не помню когда. Может быть году в 15-ом, может раньше. Скажу сразу. Не интересовался, не читал. Страничку вики открыл только сейчас. И представление моё о нём было как некий язык, со своим синтаксисом и чудо компилятором, который что-то чудесничал и делал сразу байт код мимо стандартного java-компилятора. Не знаю, почему так думал. Может где-то прочитал, может не мог поверить, что великий Kotlin это тот же Groovy. Не знаю. Забегая наперёд – это всё-таки ещё один Groovy. Вот в таком вот приподнятом настроении я открыл https://start.spring.io/.

IDE

Приподнятое настроение закончилось ещё до того как я написал свою первую строчку кода на Kotlin-е. Когда я два или три дня потратил на интеграцию Kotlin-воркспейса в Eclipse. И честно признаюсь, у меня уже была стойкая и разумная мысль послать его к чертям и захерячить всё на Java. Сей разумный шаг не дала совершить моя профессиональная гордость. Я, если честно, так и не понял, в чём заключалась проблема. Сначала накатил честный плагин из маркет плейса. И проект упал. Я за 10+ лет такой ошибки в эклипсе не видел. Не могу заскриншотить её. Не думал, что буду писать это, так что не собирал скрины. Помню красный крест при открытии “Package Explorer”. Интернет плохо помогает в этом. Я даже писал отдельную тулзу для переименования файлов плагина (добавлял расширения “.class”) – не помогло. Пофиксил только установлением ещё одного плагина, где присутсвует имя Kotlin. Их там два вроде всего было. И вот с этим всем барахлом эклипс не очень хорошо себя чувствует. Часто уходит в норэспонс, особенно, когда пользуешь контекстное меню. При этом съедается CPU безбожно. Периодически приходится перезагружать IDE.

RedCode

Несмотря на то, что приложение успешно компилировалось, запускалось и работало, некоторые строки кода эклипс так и не распознавал. Дико на них ругался и чёркал красным. Я не сильно заморачивался, чтобы это пофиксить. Может и можно. Мне было некогда и неинтересно.




Static

Первое, наверное, с чем я столкнулся это статические методы. Здесь разработчики Kotlin-а поступили ещё лицемерней, нежели разработчики Java. Если разработчики Java везде где только можно вопят о том что Java это объектно-ориентированный язык, при этом скромно умалчивают о процедурном бекдоре в виде модификатора static, то разработчики Kotlin-а решили вопрос кардинально – запретили этот модификатор! Совсем. Но статический доступ есть. Через одно, извините, место.

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

NonNull

Ещё одна параинодальная фича. Среди конфигураторов спринга когда-то бытовало мнение, что NPE это плохо. И возможно оно так и было лет эдак 10+ назад. Сейчас даже для спринга нет такой проблемы. К тому же в последних Java версиях эта ошибка довольно качественно описывается. Так что для меня как программиста проблемы null не существует. Вот в принципе. Все возможные NPE должны отлавливаться на этапе тестирования. А для меня в программе null быть обязан. Особенно в высоко нагруженных системах, где создавать объекты-пустышки или обёртки слишком дорого. null вполне себе однозначное определение состояния объекта. В Kotlin-е зачем-то решили это как-то фиксить. Думаю, это была модная фича на заре становления этого языка. Дабы привлечь конфигураторов с секты спринга и прочих блаженных мечтателей избавления от шайтан-нуля.

В Котлин-е подошли прямолинейно. Где бы ты ни инициализировал переменную, ты обязан определить может ли она принимать значения null или нет. Я не знаю чем думали такие как Тагир изобретая этот подход, но применять на практике мне было не удобно от слова совсем. Наверное предполагается иметь в голове совершенно продуманную архитектуру вашего приложения вплоть до того будет та или иная переменная нулём или нет. И только после этого начинать писать код. Я, к сожалению, так не умею. И мне было ооочень некомфортно периодически скакать по классам и править уже написанный код. И хорошо, когда это прицепом не тянет интеграцию с фрейморками. В таком случае застряёшь на некоторое время, думая как это пофиксить. Не понял, не принял.

Замыкания

Замыкания, кложи, ламбды. Я не знаю, как это называется в Kotlin-е, но, думаю, что это всё очень весело выглядело до Java 8. Сейчас же это выглядит вполне себе уродски, нелогично и интуитивно непонятно. Вот такое, например:

Что такое Unit и почему я должен указать его, чтобы проигнорировать результат выполнения?.. И почему я должен активизировать лямбду так же как создаю новый объект? И вообще это что, создание объекта или вызов метода?!

А здесь про “лаконичность”:

Непросто возврат, а ещё и нужно указать, откуда именно возвращать. Хз для чего нужно было городить такой огород…

К “лаконичности” ещё можно отнести имена класса: Scanner::class, Scanner::class.java, javaClass.

Коллекции

Здесь тоже, я считаю, разработчики Kotlin выстрелили себе в ногу преследуя цель сделать что-то лучше, нежели ребята из Sun\Oracle. Не буду закапываться по мелочам. Скажу моё общее впечатление, что легко юзать Java коллекции у меня не получилось. Постоянно приходилось искать те или иные воркэраунды в, казалось бы, примитивных операциях. А разбираться в скудном предложении от Kotlin-а мне не хотелось.

Cast

Думаю, уже можно сложить мнение как протекала разработка, было открыто много страничек в браузере. Сложилось впечатление, что Kotlin не особо описан в интернете. Решения если и находились, то непрямые и с трудом. Но больше всего времени было потрачено на поиск причины бага, связанного с приведением типа. К сожалению, подробных деталей я уже здесь не приведу – давно это было. Что помню. Значит, данные у меня хранятся в файле в виде обычного json. При старте, я выгружаю json (список моих фильмов) ну и далее пользую по коду. И вот при выгружении данных я приводил их к моему типу. Kotlin это пропускал без ошибок. Всё типа ок. Ну а далее по коду, когда я пытался итерировать по списку моих данных, Java ругалась, что не может закастить мапу к моему типу объекта. Вот тут началось. Наверное, я испытывал панику спринг конфигуристов на заре зарождения сего чуда. Ошибка вроде понятна. Но абсолютно не понятно почему. Ведь надеешься на то, что Kotlin принял приведение к типу. В процессе поиска я и нашёл компилированные Java файлы.

Как можно наблюдать ничего нового. Тот же Groovy. Последний можно перегружать в рантайме. Можно ли такое делать с Kotlin – не знаю.

Вообщем, каким-то чудом, я докопал до того что из json вылазит не мой объект, а мапа, которую Kotlin игнорит и спокойно позволяет кастить к чему угодно.

 

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

Another surprise from Java

There is a javadoc for System.exit(int):

And based on this I made a simple java app:

I expect to see “$ system exit” only since System.exit(int) “terminates the currently running Java Virtual Machine”. But app never stop instead. The cause in shutdown hook and that’s specified in javadoc for method Runtime.exit(int):

 

Осторожно с mockito

Казалось бы, популярный, уже оттестированный фреймворк. И тут те на!
Допустим нам нужно замокать нечто вроде:

Дальше в тесте:

Согласно моим ожиданиям, этот код должен свалиться с NPE, потому как происходит анбоксинг после вызова метода getInt. На самом деле mockito возвращает 0.
Проверялось на mockito-all:1.10.8 и 1.10.19.
Со стороны mockito всё нормально. “Прикрылись” фиговым листком джавадоком:
By default, for all methods that return value, mock returns null, an empty collection or appropriate primitive/primitive wrapper value (e.g: 0, false, … for int/Integer, boolean/Boolean, …).

Генерация Kerberos заголовка

Недавно понадобилось мне получить Kerberos токен в джаве. До сих пор я имплементил только серверную часть Kerberos аутентификации. И там токет брался из заголовка запроса, что приходил из браузера. То есть меня не парило кто его генерирует. Как ни странно, но вменяемого описания как получить токен я не нашёл на просторах интернета. Пришлось лепить самому. Получилось такое:

Чтобы запустить это приложение нужна одна зависимость:

А также создать файл login.conf:

и запускать приложение с параметром -Djava.security.auth.login.config=login.conf

Замечание

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

ArrayList не совсем Array

Недавно столкнулся с необычным поведением у java.util.ArrayList. A именно метод java.util.List.add(int, Object). Предлагаю рассмотреть на примере:

данный код свалится с эксепшеном

Но этот же код отлично отработает с нативным массивом:

Казалось бы в чем разница? Может ArrayList инициализируется по своему? Проверил, все в порядке создается настоящий внутренний массив размером который указывается в конструкторе:

Остаётся метод add:

Как мы видим сразу же идёт проверка диапазона:

Другими словами когда мы используем конструктор java.util.ArrayList.ArrayList(int) мы как бы получаем доступ до внутрянки, одновременно с этим ничего больше не получаем взамен. На мой взгляд несколько намешано в архитектуре. Выставлен конструктор только лишь для того, чтобы оказать влияние на внутреннее предствление. И всё.

Так что в очередной раз можно утвердится в мысли, что ArrayList в Java это больше список нежели массив…

Зри в корень: java.lang.Object

В Java в вершине иерархии классов лежит класс java.lang.Object. Лежит и лежит, долгое время я им совсем не интересовался.

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

Однако, такой класс есть и я приведу тут исходники java/lang/Object.java, опустив javadoc, и попытаюсь пролить свет на некоторые моменты связанные с реализацией jvm:

Что бы я хотел отметить в этом коде?

Всего в Object 11 публичных методов, 5 обычных и 6 с нативной реализацией.

Рассмотрим обычные методы, так как их код уже доступен.

По дефолту все объекты сравниваются на равенство ссылок. Мне, кстати, в своем время понравилась шутка про то, что для того, чтобы запутать C++ программистов указатели в Java названы ссылками.

toString тоже не содержит ничего необычного, кроме разве того, что hashCode() преобразуется в шестнадцатеричную строку. И если бы apangin не написал, что нынче как только нельзя посчитать hashCode, я бы подумал, что раньше Java программисты могли найти свой объект по hashCode, т.к. он был не чем иным как ссылкой. Те 32 битные времена для многих прошли, и теперь даже не знаю, есть ли смысл в toString() выводить hashCode.

Кроме того, что wait относится к примитивам обеспечивающим многопоточность, хочется отметить бесполезность параметра nanos.

В некоторых случаях он просто добавляет одну милисекунду. Интересно, это закладка на будущее или уже есть системы в которых у wait(long timeout, int nanos) другая реализация.

Завершает парад обычных методов в java.lang.Object:

Этот метод ничего не делает, и есть куча материалов о том, что следует избегать его использования finalize и Finalizerсмысл finalize.

Теперь посмотрим на на java/lang/Object.class Например, мне интересно что в нем указано в качестве супер класса. Находим в установленном jre или jdk rt.jar, распаковываем:

И видим, что в super class у него прописаны 00 00, интересно что будет, если руками создать class файл без супер класса.
Я взял Hello.class из моей предыдущей заметки.

Открыл его в vim и заменил содержание буфера на hex дамп vim.wikia.com/wiki/Hex_dump:

Поразился мощи vim редактора. Быстренько нашел байты для super_class. Напомню, они лежат согласно спецификации через 4 байта после окончания constant_pool. Конец constant_pool ищется по тегу строки 00 01 и последовательности не нулевых байтов, когда начинаются нули идут другие разделы constant_pool. Для других class файлов это может быть не так, но в моем случае сработало.
Возвращаемся обратно к бинарному виду:

Сохраняем изменения. Запускаем наше поправленное приложение:

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

Нам нужны исходники jdk. Я выбрал OpenJDK для исследования. Будем качать их отсюда:

hg.openjdk.java.net/jdk8/jdk8

И хранить в Меркурии:

hg clone hg.openjdk.java.net/jdk8/jdk8

Но на этом не всё.

Надо еще запустить:

И подождать. Отлично, исходники скачались и можно искать нашу ошибку. Я делаю это grep-ом:

Открываем classFileParser.cpp и там на 3095 строчке:

Нас интересует вот эта часть:

check_property лежит в заголовочном файле classFileParser.hpp и выглядит так:

Я стал искать где выставляется _need_verify и за что отвечает. Оказалось в classFileParser.cpp есть вот такая строчка:

verify передается при вызове:

Этот метод вызывается во многих местах, но нас интересует в hotspot/src/share/vm/classfile/classLoader.cpp:

Как же устроен should_verify_for в hotspot/src/share/vm/classfile/verifier.cpp:

Так как в should_verify_class мы передаем false, смотрим BytecodeVerificationLocal в hotspot/src/share/vm/runtime/arguments.cpp:

Зарываясь дальше можно найти черную магию с макросами в hotspot/src/share/vm/runtime/globals_extension.hpp:

Но меня это пока не интересует. Мне надо выяснить значение BytecodeVerificationLocal, в случае когда jvm стартует без параметра -Xverify. Это можно найти в коде, но мне кажется, сейчас не уместным лезть дальнейшие дерби и пора выбираться. Документация в помощь. По дефолту jvm запускается с параметром -Xverify:remote и BytecodeVerificationLocal будет false.

Значит _need_verify тоже false и в check_property вызывается assert_property(property, msg, index, CHECK) с параметрами false, «Invalid superclass index %u in class file %s», 0, CHECK_NULL.

Собственно, здесь и выбрасывается сообщение об ошибке. Теперь посмотрим на fatal(msg), чтобы узнать как это делается.
Хотя, на часть вопроса мы уже ответили. Нельзя сделать classfile в котором для поля super_class будет значение 0 и загружать его с помощью дефолтного ClassLoader.

Итак, fatal определенный в hotspot/src/share/vm/utilities/debug.hpp:

hotspot/src/share/vm/utilities/debug.cpp:

Реализация report_and_die() в hotspot/src/share/vm/utilities/vmError.cpp нетривиальна, но из нее следует, что в Java мы уже не возвращаемся и выводим сообщение об ошибке из недр jvm. На этом я хочу переостановить исследование jvm и java.lang.Object.

Выводы

java.lang.Object особый класс, имеющий уникальный class file, в котором в качестве суперкласса не указан ни один класс. Создать такой же класс средствами языка Java нельзя, но также затруднительно, если вообще возможно, сделать это и манипуляциями с байтами class файла. Надеюсь у меня получилось передать часть восхищения, которое я испытывал исследуя исходники jvm. Призываю всех попробовать сделать то же самое.

Источник

Load your CLASSPATH at runtime

One of the great advantages of programming in JAVA is the ability to add to your code as you go. You can create classes and turn them into packages which can then be imported and used in any other JAVA application. Each instance of your program relies on the CLASSPATH environment variable for external packages. For example you can import an external JDBC driver for your application to connect your database. You can also import dynamic modules to add and change program functionality. Setting the classpath variable can be done as a part in the environment variables or while invoking the JVM. However, when your program relies on external web updates, it’s not practical to change the command arguments every time there is a major update. In this article, we are going to explore programmically changing your classpath to modify program features. We are going to assume you have an updater module that basically drops all of your JAR files into the module’s directory. We are then going to read all the files in the directory and add them to your classpath at run time. The code to reading all the files in the directory looks like this:

And the last step is to append the files to the classfile.

It is very important to program your classpath as the first step when you invoke your program. Any objects that are being initialized before the classpath is set can result in a no class definition error. You’ve seen how useful it can be to any software that relies on external packages. The possibilities are endless. Happy programming!

Source

Как я писал Android приложение

Не думал, что захочется поделиться опытом по написанию моего первого функционального Android приложения. Когда начинал его писать, думал, что всё будет просто, ведь, блин, их уже то понаписано тьма! Однако мне пришлось спотыкаться на ровном месте, причём не раз!

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

Стартап за 2 дня

Опыта работы по написанию приложений для Android у меня не было. Вернее, ещё в 2011, помнится, пытался баловаться плагинчиком для Eclipse. Но много лет прошло. Плагинчик уже не модный, а модна Idea. Вот и пришлось тыкать в новые кнопки и в новых окошках (не люблю я Idea). Тут же можно добавить всякоразные там проблемы с Android API, инициализацией проекта в IDE и другой сопроводительной активностью.

Тем не менее, за два дня функционал был готов. Я мог загружать список всех новостей и просматривать отдельную из них. С меня потребовалось всего лишь дамп HTML с сайта и его парсинг. Google дал информацию как это всё организовать в приложении. Ну а новостной сайт позволял это своей архитектурой.

Немножко напильником

Само собой, нужно было слегка подпилять, что было сделано за два дня. Я особо не заморачивался с красивостью кода, тестами и прочими тормозами стартапа. Пришлось всунуть jsoup  для парсинга HTML, когда сабстрингать стало уже больно. Повозился с картинками, чтобы их ширина выравнивалась под размер экрана. Вырезка ненужного HTML и рекламных ссылок. Плюс один юнит тест. Неделя ушла на всё это.

Всё работает, но нет

В итоге у меня есть приложение, которое работает в эмуляторе. И даже впихнул его себе в телефон. Но вылезла беда. Если в сети WiFi оно ещё работает более-менее, то в мобильной сети оно совсем отказывалось вменяемо работать. Вернее, могло грузить список новостей несколько минут и то, не было гарантии, что все прогрузится. Причем речь шла о нескольких киллобайтах текста. Начались наблюдения. Оказалось, что у одного оператора тупит, у другого оператора почти не тупит. Примерно та же ситуация наблюдалась и при попытке зайти на сайт через браузер. 3G, 4G никакой роли не играли. На прямой вопрос у оператора: вы, уроды, сниффите? Ответ: нет конечно же! как вымогли подумать такое! Но факт есть факт. На все остальные сайты заходит вменяемо, но вот на мой, с новостями – тупит. Законная сторона меня лично не парит. Во первых, симка предоплатная, во-вторых закон я не нарушаю. Так что, стукачу оператору и рыцарям плаща и кинжала – физкульт привет! Парит меня, что работает никак моя аппликашка! Несколько месяцев я мучался, переключал симки в телефоне, но потом немножко разгрузился с текучкой и вернулся к разработке. Первым делом нужно было устранить сниффинг. Google плюс мой сайт с поддержкой PHP и проксик готов. Теперь контент грузится на обеих операторах одинаково, а на моём сайте заработал счётчик с посещениями. Ещё раз всем им там привет!

Грузи, грузи

За время, которое прошло, у меня на машине появился Docker. И это чудо не захотело дружить ни с VirtualBox, ни с Android эмулятором. Времени, а главное желания разбираться, в этом гауне не было. Пришлось разработку перенести на другую машину, попутно укрепив ненависть к Docker.

Чем парсишь, Вань

Всё. Новости вроде грузит нормально теперь, но вот если открываешь отдельно взятую – боль! Иногда грузит, но долго, иногда чёрный экран и ничего. Предположение первое – опять сеть. Дебаг мне в помощь и… Не сеть! Парсинг! Пришлось рефакторить парсер и мерять. Получив цифры для jsoup, я начал допиливать его конкурент на сабстрингах. И уже при частичной реализации парсинга я получил цифры 420 против 24. Я  охренел – более чем на порядок отличие! Попытался поискать аналог jsoup, нашёл какой-то noname, но и его цифры (около 120) меня не соблазнили. Кроме того, у него был более тупой API чем у jsoup. Отсюда вынужденное решение – переписывать всё на сабстринги с последующей миграцией в StringBuilder. Уже чувствую вибрацию приверженцев секты “красивый код”, но мне, чёрт возьми, нужно было работающее приложение, а не красивый код внутри хреново рабочего! Много пришлось повозиться со всякими там циклами, скобочками и прочей мелочёвкой. Университет вспоминался часто с курсом основ алгоритмизации! Никаких новых алгоритмов! Упаси бог! Просто – необычные и запутанные циклы с индексами. Много циклов и индексов. Месяц ушёл на всё это.

Ща как запиздячу

Внутренне удовлетворение достигнуто. Но чхать моё приложение хотело на моё удовлетворение. Один хрен криво работало. Иногда новость подгружается весело и задорно, иногда тьма на весь экран, но с большой вероятностью подгрузки. Такое ощущение что ресурс у операционки занят и мой запрос на сервер что-то ждёт где-то в дебрях Android. Воркэраунд для этого вроде очевидный – запилить пул тредов и в фоне загружать новости в кеш, пока я читаю первую новость или просматриваю список новостей. Здесь было две попытки. Первая привела к стохастическому свалу всего приложения без объяснения причин. После того, как обернул код загрузки новости в try-catch-Throwable, проблема исчезла.

Итог

Через пол года у меня есть в телефоне приложение, которое:

  1. Работает.
  2. Меня не бесит.
  3. Я его написал сам.
  4. Есть куда по-чучуть что-то улучшать в будущем.

Мораль

Я не вспомнил о куче мелких “камушков” которые занимали время. Иконка, цвет кнопки, картика “под краешек” экрана и прочая мелочёвка. Хочу сказать, всё оно через Ж делается в Android. Такое вот втутреннее ощущение, что рудимент и рудиментом погоняет. Работа с Android помимо самой Java, требует ещё определённой сноровки и в самом фреймворке. Нельзя снисходительно джавистам отноститься к андройдщикам!

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

До сих пор остаётся открытым вопрос: написал бы я приложение, если бы уделял внимание всякоразным бестпрактисам, юнит тестам и TDD (шепотом и восхищённо)?

Также я бы подчеркнул, что разработка приложения это не есть непосредственно девелопмент с тестированием. Очень большую роль играет деливери и сопровождение. В моём случае, как видно, это были ключевые звенья в разработке. Никакими бестпрактисами я бы не отловил проблемы с мобильной сетью! Так что, как это может ни прискорбно для многих прозвучать, но будущее за “хуяк, хуяк и в продакшн”(c).