Генерация PDF-документации из OpenAPI-спецификации в SpringBoot-приложении
Предисловие
Данная статья может быть полезна тем, кто ищет способы автоматической генерации PDF-документации для описания API разработанного SpringBoot-приложения
Описание проблемы
При интеграции с нашим приложением, написанном на «классическом» SpringBoot-стэке встал вопрос о предоставлении описания API партнеру. Практически из коробки SpringBoot позволяет развернуть на стороне Вашего приложения тонкий Swagger-клиент и сгенерировать на лету спецификацию в формате Swagger (OpenAPI), которая представляет собой JSON особой структуры (хотя если читатель не знает, что это, наверное нет никакого смысла вообще читать эту статью).
Проблема осложнялась тем, что наш партнер разрабатывал на 1С, и во всех современных спецификациях для него были слишком сложно освоиться, поэтому встала задача предоставить документацию в человекориентированном виде — DOC, PDF и прочее. Я боюсь представить, в какой бы шок его повергла спецификация в формате WSDL (но это я уже отклоняюсь от темы).
В ходе изысканий в google, была найдена статья — https://www.baeldung.com/swagger-generate-pdf, и советы на stackoverflow, которые фактически повторяли эту статью. Фактически было найдено всего 2 варианта решения:
1) Использовать онлайн-конвертер https://www.swdoc.org/
2) Настроить цепочку из 3 maven-плагинов:
swagger-maven-plugin генерирует swagger-спецификацию
swagger2markup-maven-plugin на основе swagger-спецификации генерирует документацию в формате ASCII-doctor
И уже asciidoctor-maven-plugin генерирует на основе ASCII-документации PDF-документацию
Для минимизации трудозатрат был выбран первый вариант — просто генерировать PDF-документацию в онлайн-конвертере. И на какое-то время показалось, что проблема решена.
Однако вскоре выяснилось, что онлайн конвертер корректно работает только со спецификацией в формате Swagger V2, которую генерируют наши legacy-сервисы, использующие для поддержки swagger’а библиотеку springfox. Большинство новых сервисов используют для этих целей библиотеку springdoc, которая уже генерирует спецификацию в формате Swagger V3 (он же OpenAPI 3). При попытке генерации PDF из спецификации этого формата, одноименный чекбокс на сайте онлайн-конвертора спасал мало — PDF-файл либо вообще не генерировался и выскакивала ошибка, либо конвертировался кривой и «терял» много данных из спецификации, например информацию о типах в POST-методе или описание отдельных полей. Было сделано предположение, что сайт работает не совсем корректно, и было решено настроить необходимые maven-плагины погрузиться в кротовую нору. На первый взгляд казалось, что дело буквально на 20 минут — зашел и сразу вышел скопировать конфиг плагинов и запустить команду «mvn package»
Нужно было добавить какую-то картинку, чтобы не было так скучно читать простыню текста
В кротовой норе
Итак, еще при первоначальном поиске решения был найден совет, что swagger-maven-plugin по генерации swagger-аннотации не очень то и нужен, хоть это и указывается практически везде. Достаточно написать тест, в котором поднимается Spring-контекст, и скачивается именно та спецификация, которая генерируется библиотекой springdoc, и этот файл сохраняется в каталог сборки для дальнейшей его обработки. Это исключает шанс того, что swagger-maven-plugin обрабатывает swagger-аннотации не совсем так, как springdoc.
@Slf4j
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PersistOpenApiTest {
private static final String PERSIST_PATH = "target/api-doc.json";
@LocalServerPort
int localPort;
@Value("${springdoc.api-docs.path}")
String apiDocPath;
@Autowired
ObjectMapper mapper;
RestTemplate restTemplate = new RestTemplate();
@Test
public void persistOpenApiSpec() throws Exception {
log.info("loading openApiSpec");
String openApiSpec = restTemplate.getForObject("http://localhost:" + localPort + apiDocPath, String.class);
log.info("openApiSpec is {}", openApiSpec);
Assertions.assertNotNull(openApiSpec);
Files.writeString(Paths.get(PERSIST_PATH), prettyJson(openApiSpec));
}
private String prettyJson(String json) throws Exception {
var mapSpec = mapper.readValue(json, Map.class);
return mapper.writer().withDefaultPrettyPrinter()
.writeValueAsString(mapSpec);
}
}
Написать такой тест не составляет никакого труда.
Следующий этап — настроить плагин swagger2markup-maven-plugin, который на основе swagger-спецификации генерирует документацию в формате ASCII-doctor. И тут начинается самое веселье.
Оказывается, swagger2markup-maven-plugin требует 2 зависимости, которых нет в репозитории maven-central. Точнее данные о них есть, но указано что они находятся в репозитории Jcenter, который прекратил свое существование в 2021 году (если кто не знал), и по факту их нигде не сохранилось… В github-проекта swagger2markup было найдено issue об этом, но никакой обратной реакции от разработчиков не было.
Но нам повезло — один jar-файл удалось найти на каком-то китайском сайте, и даже при помощи переводчика найти, какой иероглиф означает «скачать», и сохранить эту библиотеку. Со второй библиотекой ситуация немного сложнее, пришлось скачать исходники, немного освоить работу с gradle, выпилить из проекта все ссылки на jcenter, и собрать библиотеку из исходников.
На всё это дело ушло часа 3–4, началось дейли, и ПМ заявил, что я занимаюсь ху… ерундой, легче заставить наших партнеров-специалистов по 1С работать напрямую со OpenAPI-спецификацией в формате JSON.
Таким образом можно сделать вывод, что на любимом всеми сайте baeldung.com, на stackoveflow и в других источниках рабочего варианта решения проблемы просто нет (либо мне его найти ну удалось).
Шутка, было бы глупо, если бы на этом история завершилась. Данная проблема показалась мне уж больно интересной и нестандартной, да и я чувствовал, что уже был практически в шаге от её решения. Так что пришлось продолжить ее решать в свободное от работы время, в выходной день Вашего покорного слуги типичного быдлокодера.
Оставалось совсем немножко — настроить последний плагин asciidoctor-maven-plugin и запустить сборку. Так как мы модные и стильные, взяли не ту дремучую версию из статьи на baeldung, а последнюю доступную версию в maven-репозитории. Тому, что ничего не собралось, и плагин упал с ошибкой, я уже не удивился, немного поискал и нашёл рабочий пример в официальном репозитории asciidoctor-maven-plugin на github, оказалось, что по умолчанию плагин подтягивает версию ruby, с которой сам же и конфликтует, но на удивление в репозитории с примерами это учли и переопределили на рабочую версию ruby. Почему в самом плагине используется нерабочая версия ruby, остается тайной, покрытой мраком. Чем больше погружаешься в мир OpenSource-технологий, тем больше возникает вопросов…
По итогу, все собралось, запустилось, сгенерировался PDF-файл на основе спецификации OpenAPI 3, и мы пришли к тому, с чего начали — он был такой же «кривой», как и при генерации в онлайн-конвертере.
Оказалось, что плагин swagger2markup-maven-plugin не просто использует недоступные на данный момент зависимости, он попросту мертв — последний релиз был в 2018 году, а судя по issue в github’e, примерно до 2020 года велись работы по поддержке OpenAPI 3, но они не были завершены, и плагин поддерживает только Swagger 2. Ну, а в 2020 его мейнтейнер Robert Winkler опубликовал открытое обращение к сообществу, что в одиночку не справляется и ищет кому передать эту ношу, и судя по всему, никого не нашёл.
Тупик. Уныние и безнадежность
Итак, я вернулся к тому, с чего начал. Какие же были варианты?
Сделать fork от swagger2markup-maven-plugin, и доделать поддержку OpenApi3, судя по всему какие-то заделы уже были реализованы, но не были доведены до конца. Вопрос только, на сколько часов трудозатрат этот вариант?
Ранее в ходе поисков в google были найдены проекты на github на основе JS, уже реализующие данную функциональность, возможно стоило попытаться реализовать maven-плагин, использующий запуск этих решений?
Продолжить google-research, углубиться на 3–4 страницу поисковой выдачи и надеяться что кто-то решил эту задачу…
Очевидно, начинать нужно с конца, так как первый вариант самый трудозатратный, второй — менее затратный, и третий с минимальными затратами. И мне повезло!
Рабочее решение
Оказывается в проекте openapi-generator существуют генераторы документации https://openapi-generator.tech/docs/generators/#documentation-generators
Ранее я уже пользовался openapi-generator-maven-plugin, но предполагал, что он способен на основе спецификации генерировать только клиент и модель классов для доступа к сервису. Однако, выяснилось, что прописав нужное имя генератора, вместо клиента он способен сгенерировать не клиент, а документацию — в формате html, html2 и asciidoctor. Интересно, что это была лишь гипотеза, нигде об этом напрямую не было написано, и ее пришлось проверять, чтобы удостовериться в работоспособности решения.
org.openapitools
openapi-generator-maven-plugin
7.1.0
open-api-doc-html
prepare-package
generate
${project.basedir}/target/api-doc.json
html
open-api-doc-html2
prepare-package
generate
${project.basedir}/target/api-doc.json
html2
open-api-doc-asciidoc
prepare-package
generate
${project.basedir}/target/api-doc.json
asciidoc
Плагин, сконфигурированный указанным выше способом — формирует документацию в 3 форматах — html, html2 и asciidoctor. Html и html2 представляют из себя уже 2 формата, удобных для восприятия человека и их можно отправлять непосредственно Вашим партнерам.
Однако, если нужен все-таки PDF, то необходимо настроить еще один плагин, который будет на основе asciidoctor генерировать PDF-файл.
2.2.4
2.3.9
2.5.10
9.4.2.0
org.asciidoctor
asciidoctor-maven-plugin
${asciidoctor.maven.plugin.version}
org.asciidoctor
asciidoctorj-pdf
${asciidoctorj.pdf.version}
org.jruby
jruby
${jruby.version}
org.asciidoctor
asciidoctorj
${asciidoctorj.version}
${project.basedir}/target/generated-doc/asciidoc
${project.basedir}/target/generated-doc/pdf
generate-pdf-doc
prepare-package
process-asciidoc
pdf
rouge
font
-
В итоге — у нас есть 5 вариантов документации
сама спецификация в формате OpenAPI 3 (сохраняется в unit-тесте)
html
html2 («красивенький» html)
asciidoc
pdf
При желании можно поизучать настройки каждого генератора (ссылки приведены в комментариях в конфигурации плагина).
Исходный код демо-сервиса и настроенные плагины выложены в github https://github.com/shmakovalexey/sw-pdf-example/blob/main/service/pom.xml
PS: у читателя может возникнуть вопрос — почему я не мог сразу описать рабочее решение, а расписал все свои мытарства? Отвечу просто — я потратил не один час рабочего времени, и еще намного больше личного времени, чтобы прийти к рабочему решению, так что думаю будет справедливым, если читатель почувствует хоть малый отголосок этой боли. А для англоязычного сообщества — я описал найденный мной способ в самом популярном вопросе на Stackoverflow на эту тему.