Как мы мигрировали на 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-а. Хотя его там никогда не было и нет до сих пор. Такое…

Включаем спринговые логи

Spring как и любой другой сложный фреймворк умеет сам себя логировать. Иногда нужно уметь смотреть что там внутри происходит. Поэтому давайте попробуем разобраться, как можно заставить его говорить больше, чем он умеет по умолчанию.
Поставляется Spring со встроенным логгером JCL. По сути, это ранний аналог sf4j. Не сам логгер, а обёртка над различными имплементациями. Отсюда наша задача сводится к настройке JCL и стандартного Java логгера. Вполне, думаю, возможно подключить и другие реализации, но это всё зависит от конкретной задачи. Наша же заключается в том, чтобы Spring куда нибудь вывел дебажную информацию о себе. Поэтому Java логгера будет достаточно.
Итак. Задача сводится к следующим трём пунктам:
1) Нам нужно сказать JCL о том, чтобы он подключил Java логгер
2) Настроить Java логгер
3) Получить логи
Давайте теперь постараемся всё это реализовать:
1) Реализуется очень просто. Нужно всего лишь положить файл commons-logging.properties в корень нашего класспаса. Содержимое его должно быть:

Но, если копнуть глубже, и посмотреть внутрь JCL, то мы увидим, что это лишне, поскольку он сам по умолчанию использует Java логгер.
2) Создадим теперь файл для настроек Java логгера. Положите его в любое место и добавьте следующее содержимое:

Обратите внимание на строку 7. С её помощью можно получать логи из того пакета, который именно мы захотим.
3) Теперь чтобы это всё заработало нужно Java логгеру сказать, чтобы он настроился из того файла, который мы только что создали. Это можно сделать через системную переменную:

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

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

Полезная ссылка