Где-то раз в год возникает желание посмотреть: если сейчас начинать приложение с нуля, то что бы обязательно в него включил? Кроме очевидной практической ценности (обычно начинается новый проект) это так же позволяет осознать какой архитектурный опыт получен за последнее время.
Необходимость шаблона сверх https://start.spring.io вызвана следующими типами доработок:
Выбор технологий. Да, всегда пытаешься выбрать как-то осознанно, но, в целом, многие варианты подойдут. Тем не менее нужно что-то выбрать. Как из набора start.spring.io, так и дополнительно. Тут технологии как базовые, так и для улучшения опыта разработки (вспомогательные).
Необходимо связать все компоненты вместе. Именно этому и посвящен Spring Boot и start.spring.io. Тем не менее, из кода будет видно, что этого недостаточно и нужно еще писать свои связки.
Лучшие значения по умолчанию: например, непонятно в какой вселенной по умолчанию нужно моментально завершаться, а не постепенно завершать обработку запросов. Это одно из самых ярких, но параметров довольно много изменено.
Примеры: хорошо сразу видеть базовые примеры использования технологий. Можно практически сразу из реального приложения их удалить, тем не менее они останутся в репозитарии шаблона. Это набор лучших практик показывает как обычно нужно кодировать тот или иной случай.
Разницу между https://start.spring.io и тем, что получилось после ручной доработки можно легко отследить по истории комитов.
Шаблон еще хорош тем, что позволяет достаточно легко ответить на вопрос "а что если применить технологию ХХХ?" Например, "а что если для GraphQL использовать библиотеку DGS?" Шаблон небольшой и публичный, но при этом есть код, который реализует типичные эндпоинты — можно поэкспериментировать не отвлекаясь на неважные в эксперименте детали.
Так что, если есть предложения по улучшению — fork & commit. Покажи свое кунг‑фу:).
Далее в отдельных секциях будут идти архитектурные решения. Это краткое обсуждение какой-то темы, принятое решение и какие альтернативы рассматривались. По большинству тем в моем блоге есть отдельные заметки с более развернутыми аргументами, если вдруг заинтересует (а то и так получается весьма длинно). В конце ссылка на репозитарий с примером кода по этим решениям.
Язык: Kotlin
Варианты: Java, Kotlin, Rails, Python, Golang, Node.js.
Котлин показывает лучший баланс между легкостью и читаемостью кода и быстротой и стабильностью платформы. В блоге есть несколько статей более детально посещенных этой теме.
Фреймворк: Spring Boot
Варианты: Spring Boot, Quarkus.
Quarkus дает лучший опыт использования для разработчиков (быстрая сборка, тематические руководства, легкая поддержка нативной сборки).
При этом, к сожалению, Quarkus отстает в поддержке ассинхроности в общем и для Котлина (корутины) в частности (например, в GraphQL). И ключевые новшества (http-клиенты через интерфейсы и нативные бинарники) Spring Boot перенял.
С учетом большей популярности среди разработчиков Spring Boot выглядит хорошим вариантом.
Стиль: асинхронные Kotlin co-routines
Варианты: синхронный, асинхронный Reactor, асинхронный Kotlin co-routines.
Сейчас нет смысла начинать приложения на синхронном стиле, т.к. асинхронный обладает достаточно очевидными преимуществами и уже хорошо поддерживается языками и фреймворками. Корутины намного читабельнее стиля Rx, поэтому их и используем.
БД: Postgres
Варианты: Postgres, MySQL, Oracle, MongoDB.
Postgres как решение по умолчанию. Детальней в одной из заметок на блоге.
SQL API: Spring Data и jOOQ
Варианты: Hibernate (aka JPA), MyBatis, EclipseLink, querydsl, JetBrains Exposed, Panache, jOOQ, Spring Data R2DBC.
В общем и целом, все варианты плохи. Возможно, тема слишком сложная.
jOOQ выглядит хорошо, но есть ощущение, что ресурсов или альтернативного пути развития не хватает — сложен для простых случаев.
Для простых запросов хорошо подходит Spring Data.
Миграции БД: в приложении SQL-файлы через Flyway
Варианты: в приложении, внешнее; Flyway, Liquibase.
Если исходить из принципов CICD, то миграции должны быть внутри приложения, чтобы применяться автоматически.
Flyway хорош поддержкой sql-файлов. Отмены принципиально не нужны — миграции идут только вперед. Если для разработки нужно откатиться, то можно пересоздать соответствующую базу с нуля.
Система сборки: Gradle Kotlin
Варианты: Maven, Gradle, Gradle с Kotlin.
На самом деле не принципиально, но в Gradle попроще добавить кастомный функционал, который иногда нужен.
Если уж мы используем Котлин, то почему бы и в Gradle его не использовать (чтобы только ради Gradle не использовать/учить другой язык).
Система контроля версий: Git
Варианты: CVS, SVN, Git, Perforce, Mercurial, ClearCase, Team Foundation.
Обычно всех устраивает Git. В деталях можно придираться и, надеюсь, когда-нибудь улучшат его (особенно в способах подключать один репозитарий в другой), но ради этого менять саму систему нет смысла.
Если сейчас кто-то использует что-то другое, то это "исторически сложилось" и менять слишком дорого.
UI системы контроля версий: Gitea
Варианты: Microsoft GitHub, Gitlab, Gitea, Atlassian Bitbucket, JetBrains Space.
Серьезно выбор был между 2-мя Open Source-версиями: Gitlab и Gitea, т.к. остальные варианты дороги для self hosting, да и не предоставляют достаточно функций относительно бесплатных вариантов.
Gitlab более развитой, но Gitea его быстро нагоняет и уже очень приятный продукт. При этом Gitlab коммерческая разработка и интересные функции вынесены в платные версии, что не так в случае с Gitea. И тоже важно — Gitea написан на Golang, а Gitlab на Rails. Из-за этого Gitea быстрее и у него меньше системные требования.
CICD движок: Gitea
Варианты: Drone, ArgoCD, Gitea.
У Gitea движок аналогичный GitHub/Gitlab, так что смысла искать что-то другое особого нет. Если из-за текущих ограничений не понравится (эта подсистема появилась относительно недавно и активно развивается в Gitea), то можно временно использовать что-то другое (Drone).
Анализатор качества кода: SonarQube, Ktlint
Варианты: detekt, ktlint, sonarqube.
sonarqube находит множество проблем качественно. ktlint больше по форматированию, но тоже находит хорошо и используется.
detekt по умолчанию находит как-то слишком много сомнительных вещей. Еще присматриваюсь к нему, пока что нет в шаблоне.
Редактор: Idea Ultimate Edition
Варианты: Idea SE, Idea UE, VS Code.
Пока что Idea UE вне конкуренции, но есть надежда на VS Code еще через несколько лет развития.
Основные дополнительные плагины:
Kotlin Fill Class
SonarLint
GraphQL
.ignore
EnvFile
Kubernetes
Технология API: GraphQL
Варианты: plain HTTP, REST, jRPC, GraphQL.
GraphQL имеет хорошую типизацию и популярность. Главное отличие от HTTP/REST — это введение нового слоя абстракции — запросы. Он не привносит заметного замедления изначально, а при развитии проекта только проявляет себя еще лучше:
можно собрать все запросы, чтобы понять используется что-то или нет
клиенты (UI) могут обновлять свои запросы без изменения сервера, что приводит к независимости и меньшему количеству технического долга
Стиль API: CQRS
Варианты: REST, RPC, CRUD, CQRS.
GraphQL сам по себе подстегивает стиль CQRS: отдельные запросы и мутации. Опять же с течением времени отдача от такого подхода только увеличивается.
Развертывание приложения: Kubernetes или Podman
Варианты: Kubermetes, Podman, Docker compose, Docker, Systemd service
Если в инфраструктуре уже есть Kubernetes, то очевидный вариант продолжить его использовать. Если нет (например, в домашних условиях), то можно использовать Podman — он так же позволяет создавать поды, аналогичные Кубернетесу, но без всей тяжелой обвязки.
Пакетирование приложения: Docker
Варианты: zip, war, jar, rpm, deb, docker.
Т.к. запускаться будет либо через Докер, либо через Кубернетес, то docker очевидный ответ.
Сборка через Jib, т.к. он не требует Docker (удобно для CICD).
Базовые версии
JVM: 17 — текущая стабильная;
Kotlin: 1.8 — текущая версия из генератора приложения;
Spring Boot 3 (Spring 6) — текущая стабильная;
Postgres: 15.2 — текущая стабильная.
Архитектурный принцип: монолит
Варианты: монолит, микросервис.
Начинать нужно всегда с монолита, т.к. так быстрее писать и исправлять. Когда-нибудь потом это можно будет разделить на микросервисы.
Понятно, что если у вас в распоряжении есть S3 API, то не нужно это самостоятельно писать. Можно использовать это как внешний микросервис.
Аналогично, если какой‑то код написан на другом языке — не нужно особо мучаться, можно обернуть его в отдельный микросервис.
При этом основной код имеет смысл оставить на первые года развития в монолите. А потом лучше будет понятно нужно ли от этого отходить и, если нужно, то как.
Например, проект Gitea — монолит и вряд ли ожидается, что он станет микросервисным когда‑либо.
Модульность: один модуль
Варианты: один модулей, много модулей.
Никаких преимуществ от опыта работы с многомодульными проектами не видел. Одни проблемы. Поэтому одного модуля достаточно.
Принцип: структура папок
Основные папки:
bin — скрипты, используемые для разработки;
docs — основная документация (потом можно выносить в вики, если документация будет активно развиваться);
deployment — файлы, связанные с деплойментом в Kubernetes или Podman;
src/main/resources/db/migration — миграции схемы БД Flyway.
Принцип: структура пакетов
Варианты: довольно много.
Основные пакеты:
client — клиенты к внешним системам (обычно http‑клиенты, но могут быть и очереди, и что‑то другое);
config — настройки приложения, Spring и библиотек;
-
db — код работы с Postgres:
dao — jOOQ: ручной код общения с Postgres;
entity — Spring Data;
repository — Spring Data;
sql — jOOQ: сгенерированный код схемы Postgres.
-
domain — доменный код:
-
«domain name» — в приложении (т.к. это монолит, скорее всего будет несколько доменов):
client — адаптер к внешним клиентам в терминах моделей домена;
model — модели домена;
service — сервисы домена.
-
graphql — код, специфичный для GraphQL;
service — сервисы уровня приложения;
utils — классы, которые в идеальном мире должны быть в каких‑то библиотеках;
web — код, специфичный для HTTP.
В целом, структура подразумевает четкое разделение кода на уровни — работа с внешней средой, технический код, бизнес‑код с учетом обычной специфики приложений.
В отличие от более жестких политик тут предполагается возможность проникновения структур данных между слоями. Это сделано из практических соображений: такого практически не бывает, что клиент полностью поменялся, а структура данных в итоге осталась той же. Даже если такое случиться, то автоматическим рефакторингом это все исправляется. Пуризм же ради пуризма приводит к лишнему коду, что удорожает развитие проекта и поддержку (т.к. это очень важная тема про это есть отдельная заметка в блоге).
Так же можно заметить, что структура пакетов не подразумевает пакетов для интерфейсов. Это из-за того, что интерфейсов практически не ожидается. Интерфейс имеет право на жизнь в данной структуре, если у него уже есть несколько реализаций (тот же принцип отсутствия лишнего кода).
Тестировать структуру пакетов можно при помощи ArchUnit. В данном стартере примера нет, но может быть когда-нибудь появится (я использую на некоторых проектах, но не могу сказать, что год мне нравится, да и ценность автоматической проверки есть только в реально больших коллективах).
Предполагается, что на каждом уровне будет 3-10 файлов. Если больше или меньше, то, в идеале, нужно корректировать структуру пакетов.
Принцип: без пакета с именем приложения
Варианты: с пакетом с именем приложения, без пакета с именем приложения.
Например, пусть пакет организации будет name.stepin
, а приложение пусть называется kotlin-bootstrap-app
. Тогда 2 следующих варианта базовых пакетов для приложения:
name.stepin
name.stepin.kotlin-bootstrap-app
Второй вариант выглядит менее удачным:
это усложняет перемещение классов из приложения в приложение, а класть все исходники в одно дерево файлов мы не планируем;
путь ограничен 255 и 21 символ мы использовали на пустом месте, в итоге может где-то потом не хватить.
Принцип версионирования: семантические версии
Варианты: семантические версии, множество других.
Для серверных приложений вполне можно использовать семантические версии, где первая цифра — сломана ли совместимость со старыми клиентами или нет (что соответствует семантическим версиям).
Дополнительные библиотеки: MapStruct? Нет
Варианты: писать самому, MapStruct.
Котлин хорошо подходит, чтобы писать это самому.
Дополнительные библиотеки: hibernate validator
Это библиотека для написания валидаторов. Обычно она нужна и ее хватает. Альтернатив не искал.
Дополнительные библиотеки: mockk
Это хорошая библиотека с полноценной поддержкой Kotlin. Имеется интеграция и с Quarkus. Особых альтернатив нет.
Сборка покрытия тестами: kover
Варианты: jacoco, kover
JaCoCo по сути является стандартом де-факто, но оно не поддерживает некоторые специфичные Kotlin-конструкции, поэтому лучше использовать Kover.
Логирование
Принципы логирования описаны в специализированной заметке в блоге.
Для логирования применяется библиотека log4j-api-kotlin, т.к. она позволяет удобно пользоваться логами:
companion object : Logging
logger.info { "hello $arg" }
Код
Репозитарий с кодом. Вариации:
сгенерированный на сайте шаблон Spring (для сравнения)
В коде несколько больше решений, чем описано решений здесь — и как с зависимостями работать (через конструктор), и как тесты писать.
Видео
В видео представлен пофайловый (более низкоуровневый) разбор шаблона. 3 части:
Об окружении
О коде
О тестах