Всем привет. В этой статье я расскажу почему нельзя просто взять готовый Maven Archetype в корпоративной микросервисной архитектуре и зачем может понадобиться изобретать свой. Статья для тех, кто хочет разобраться как кастомизировать maven архетип и сделать его более гибким.
Краткое содержание
Зачем кастомизировать архетип
Создание нового архетипа из проекта
Кастомизация архетипа
Использование архетипа
Зачем кастомизировать архетип

Зачем вообще кастомизировать архетип, и чем плохи стандартные? Есть много решений для создания готового скелета Java-приложения. Например, Spring Initialzr, список готовых maven архетипов или создание проекта в Intellij IDEA. Все эти инструменты дают нам возможность создать типовой проект с некоторыми зависимостями и дефолтными конфигурациями. Этот проект будет запускаться, но перед тем, как наполнять его бизнес логикой, нам необходимо добавлять в него наши типовые конфигурации. Например, настройки аутентификации, сериализации/десериализации DTO, настройки логирования, helm chart с переменными и шаблонами Kubernetes манифестов, CI/CD-шаблон и так далее. В этой статье мы рассмотрим, как можно сильно сократить рутинные действия с новым проектом.
Какую проблему мы решали
В нашей команде за несколько лет мы развернули без малого 30 новых микросервисов. Это не так много, чтобы задача стала рутиной, и всё делалось на автомате за 5 минут, но и не мало, чтобы каждый раз к задаче подходить как к чему-то новому. Каждый раз разработчику приходилось:
копировать конфигурационные файлы из других проектов
копировать и настраивать аутентификацию и авторизацию
добавлять стандартные зависимости
копировать и изменять Helm-чарты
добавлять стандартные архитектурные тесты
И это далеко не всё. Процесс может занимать до целого рабочего дня даже у опытного разработчика, но что хуже - легко что-то забыть. То забыли настроить метрики, то забыли про трейсинг, то забыли добавить health-check эндпоиты, или наоборот кроме health-check открыли лишние эндпоинты актуатора, что не очень хорошо для безопасности приложения.
Нам нужен был инструмент для быстрого развёртывания микросервиса, удовлетворяющего стандартам нашей команды и компании в целом. Чтобы сразу после генерации проекта его можно было пушить в репозиторий, откуда он автоматически раскатится в нужный нэймспейс кубера. Чтобы нам не нужно было копипастить конфиги аутентификации, она должна быть настроена и работать из коробки. Логи должны отправляться в ELK в правильном формате. В проекте сразу должны быть Archunit тесты для поддержания архитектуры проекта в соответствии с договорённостями команды. Таким образом, мы пришли к пониманию, что нам нужен свой архетип. Архетип который будет создавать не абстрактный скелет web-приложения, а полностью соответствующий всем нашим стандартам проект.
Какой профит мы получили
Мы используем свой архетип уже несколько лет и развернули с его использованием более 25 микросервисов. Как я уже писал, на первоначальную настройку проекта может уходить целый рабочий день. Тут профит очевиден. Понятно, что некорректно было бы говорить о том, что мы сэкономили 25 человеко-дней, ведь какое-то время ушло на разработку и поддержку. Однако на создание архетипа ушло значительно меньше времени, и что самое главное: когда на команду разработки спускается задача, требующая отдельного сервиса, работа не блокируется. Разработчики практически сразу могут брать бизнес задачи в работу.

Используемые версии
JDK 21
maven 3.9.11
maven плагины 3.4.0
Создание архетипа из проекта
Подробно о том как создать базовый архетип на основе своего сервиса описано в статье Создание архетипа Maven из существующего проекта. Тут опишу вкратце.
Создаём проект будущего шаблона со всеми нужными зависимостями, конфигами и классами. Можно воспользоваться Spring Initializr и дополнить его всем необходимым. Здесь стоит обратить внимание, что проект должен компилироваться. Далее, когда мы создадим на его основе архетип, файлы шаблонного проекта будут лежать в каталоге ресурсов архетипа, где их будет не так удобно править.
Генерируем архетип
archetype:create-from-project. Архетип попадёт в папку проектаtarget/generated-sources/archetype.Переходим в папку с созданным архетипом и загружаем его в локальный репозиторий с помощью
mvn install.
Готово! Архетип у нас в локальном репозитории. Теперь мы можем генерировать проекты командой
mvn archetype:generate \ -DarchetypeGroupId=ru.example \ -DarchetypeArtifactId=demo-archetype \ -DarchetypeVersion=1.0.1 \ -DgroupId=ru.example \ -DartifactId=demo-project \ -Dversion=0.0.1-SNAPSHOT
На выходе мы получим шаблон проекта, который запускается, но требует некоторых доработок. Например Application class будет называться так же, как класс в исходном проекте, также скорее всего вы не увидите .gitignore файл. И тут нам надо немного поднастроить архетип, чтобы в дальнейшем проекты генерировались полностью готовые к использованию.
Кастомизация архетипа
Чтобы правильно кастомизировать архетип, давайте сперва разберёмся как он работает. Это поможет лучше понять его структуру. Maven архетип - это не просто папка с файлами-шаблонами. Это полноценный инструмент со своим циклом генерации проектов.
После вызова команды mvn archetype:generate происходит следующее:
Maven скачивает архетип из репозитория
Читает метаданые архетипа и запрашивает у пользователя необходимые параметры
Создаёт структуру директорий будущего проекта
Копирует шаблоны файлов, подставляя значения переменных
Выполняет post-generation скрипт, если он присутствует
Самый важный файл архетипа - это archetype-metadata.xml.
archetype-metadata.xml
Начнём с файла archetype-metadata.xml. Это, пожалуй, главный файл нашего архетипа. В нём мы определяем какие файлы копировать в проект, как их обрабатывать, какие параметры запрашивать у пользователя и могут ли у этих параметров быть значения по умолчанию.
archetype-metadata.xml состоит из нескольких разделов. Самый большой из них - fileSets. В нём мы описываем какие файлы должны попасть в итоговый проект и как их обрабатывать.
При первой генерации проекта командой archetype:create-from-project, Maven создаст базовую версию этого файла. Её можно использовать, но она, как правило, нуждается в доработке. Например, для некоторых файлов устанавливается filtered=true, что означает подстановку переменных. Но что если этот файл в итоговом проекте должен быть с плэйсхолдером вида ${}? Такие плэйсхолдеры будут восприняты как переменные для подстановки, что приведёт к ошибке при генерации.
fileSets
В этом разделе указываем куда и каким образом должны копироваться исходные файлы проекта. Рассмотрим основные параметры секции fileSets.
Для каждого набора файлов указываем каталог в directory и шаблон файлов в include/exclude. Можно указать как полное имя файла, так и шаблон пути к нему. Например, **/*.java это любой файл с расширением .java в текущей директории и во всех вложенных директориях.
Также можно указать два дополнительных параметра: filtered и packaged.
filtered указывает на то, надо ли при генерации проекта подставлять значения переменных в плейсхолдеры вида ${groupId}.
filtered="true" - maven будет подставлять в плейсхолдеры значения переменных.
filtered="false" - maven скопирует файл как есть, без изменений. Например, если в проекте есть файл с конфигами, содержащими плейсхолдеры, которые должны заполняться переменными из других источников позже, то для таких файлов указываем filtered="false".
packaged указывает куда надо скопировать файлы. Просто в корень проекта или по пути, определённом в переменной package. Например, если переменная package это ru.example, а для файла DemoApplication.java packaged="true", то файл будет помещён по пути src/main/java/ru/example/DemoApplication.java.
<!-- archetype-metadata.xml --> <fileSets> <fileSet filtered="true" packaged="true" encoding="UTF-8"> <directory>src/main/java</directory> <includes> <include>**/*.java</include> </includes> </fileSet> <fileSet encoding="UTF-8"> <directory>src/main/resources</directory> <includes> <include>**/*.yaml</include> </includes> </fileSet> <fileSet filtered="true" encoding="UTF-8"> <directory>src/test/java</directory> <includes> <include>**/*.java</include> </includes> </fileSet> <fileSet encoding="UTF-8"> <directory>src/test/resources</directory> <includes> <include>**/*.yaml</include> </includes> </fileSet> <fileSet encoding="UTF-8"> <includes> <include>.gitignore</include> <include>.gitlab-ci.yml</include> <include>Dockerfile</include> <include>README.md</include> </includes> </fileSet> </fileSets>
Не стоит пытаться впихнуть описание всех файлов в один fileSet. Лучше создать отдельные секции для:
java файлов приложения
java файлов тестов
ресурсов приложения
ресурсов тестов
конфигурационных файлов проекта (.gitignore, pom.xml и т.д.)
Это упростит понимание и дальнейшую поддержку проекта.
requiredProperties
Этот раздел превращает генерацию проекта из копирования команды в интерактивный процесс настройки будущего проекта. Здесь мы описываем, какие вопросы Maven будет задавать пользователю и как использовать полученные ответы. Проще говоря, requiredProperties содержит переменные, которые будут использоваться при генерации проекта.
На первых порах мы использовали только дефолтные переменные groupId, artefactId, version. Однако потом поняли, что можем одновременно упростить процесс создания проекта и сделать его более гибким. Например мы добавили имя будущего Application класса, чтобы не приходилось потом переименовывать его в проекте. Также добавили параметр includeS3Client, в зависимости от которого в проект добавлялась интеграция с S3.
Здесь также есть возможность задать регулярное выражение для валидации значения через validationRegex, и возможность задать значение по умолчанию. Последнее например, полезно для определения переменной package. package хранит в себе структуру пакетов в корне будущего приложения. Эта переменная, как правило, совпадает со значением groupId, но maven просит вводить её отдельно. Приравняем package к groupId по умолчанию. А валидацию через регулярное выражение использовать для Application класса, чтобы проверить что параметр пришёл в pascal case.
<!-- archetype-metadata.xml --> <requiredProperties> <requiredProperty key="groupId"> <defaultValue>ru.example</defaultValue> </requiredProperty> <requiredProperty key="package"> <defaultValue>${groupId}</defaultValue> </requiredProperty> <requiredProperty key="mainClassPrefixName"/> <requiredProperty key="version"> <defaultValue>1.0.0</defaultValue> </requiredProperty> <requiredProperty key="includeS3Client"> <defaultValue>false</defaultValue> <validationRegex>^(true|false)$</validationRegex> </requiredProperty> </requiredProperties>
Теперь эти переменные можно использовать в шаблоне проекта. Например:
# application.yaml server: port: ${applicationPort}
// основной класс проекта package ${package}; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ${mainClassPrefixName}Application { public static void main(String[] args) { SpringApplication.run(${mainClassPrefixName}Application.class, args); } }
К переменным можно обращаться не только в коде, но и в названиях файлов. Для этого используем двойное нижнее подчёркивание. Вот как нам стоит назвать наш Application class: \__mainClassPrefixName\__Application.java
Финальный штрих: post-generation script
После того, как Maven скопировал все файлы и подставил необходимые переменные, можно добавить немного "магии". Мы можем добавить в архетип скрипт, который будет выполняться на финальном этапе генерации проекта. Это groovy скрипт. В нём можно описать всё, чего нам не хватило при создании проекта.
Для чего это может пригодиться? Например, вы хотите строго задать структуру пакетов проекта. Добавляете пустые пакеты в проект: api, configuration и т.д. Проблема в том, что они не попадут в итоговый проект, так как архетип не создаёт пустые пакеты. Можно туда добавить packageInfo файлы, но если вы не хотите видеть эти файлы в итоговом проекте, то их удаление как раз можно описать в groovy скрипте. В итоге Maven создаст все эти директории, так как они не пустые, каждая из них содержит packageInfo. А в уже готовом, сгенерированном проекте, лишние файлы удаляются на этапе постгенерации.
Этот файл должен лежать в папке src/main/resources/META-INF/archetype-post-generate.groovy.
// archetype-post-generate.groovy import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes //Удаление временных файлов packageInfo.java. Эти файлы нужны для создания пустых пакетов в генерируемом проекте deleteTemporaryFiles() void deleteTemporaryFiles() { Path projectPath = Paths.get(request.outputDirectory, request.artifactId) Files.walkFileTree(projectPath, new SimpleFileVisitor<Path>() { @Override FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.toFile().getName().contains("packageInfo")) { Files.delete(file) } return FileVisitResult.CONTINUE } }) }
В итоге получим нужную структуру пакетов без лишних файлов.
Использование этого скрипта даёт довольно большую гибкость. Его например также можно использовать, чтобы через параметры команды генерации проекта из архетипа, задавать какие модули будут в проекте. Например, добавлять какие-то типовые интеграции или выбирать тип используемой бд.
Ниже пример аналогичного скрипта, но на этот раз мы в зависимости от переданного параметра includeS3Client, оставляем или удаляем клиента и конфиги для подключения будущего проекта к S3 хранилищу.
// archetype-post-generate.groovy import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes /*Скрипт для удаления ненужных модулей в сгенерированном проекте*/ Path projectPath = Paths.get(request.outputDirectory, request.artifactId) Properties properties = request.properties String includeS3Client = properties.get("includeS3Client") String packageName = properties.get("package") String packagePath = packageName.replace(".", "/") if (!includeS3Client.contains("true")) { Files.deleteIfExists projectPath.resolve("src/main/java/" + packagePath + "/configuration/S3ClientConfiguration.java") Path directory = projectPath.resolve("src/main/java/" + packagePath + "/integration/filestorage/") Files.walkFileTree(directory, new SimpleFileVisitor<Path>() { @Override FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir) return FileVisitResult.CONTINUE } @Override FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file) return FileVisitResult.CONTINUE } }) }
Вы можете видеть, что скрипт удаляет файлы из уже созданного проекта. Именно так и реализовывается логика добавления опциональных модулей в проект. Мы добавляем в архетип все возможные зависимости и классы, а опциональность этих модулей реализуем через удаление ненужных файлов. Скрипт из примера выше подтягивает из переменных значение параметра includeS3Client, и, если значение этой переменной false - удаляет соответствующие конфигурации. Напомню, что для кастомных переменных, таких как includeS3Client, можно определить дефолтное значение, чтобы не перегружать пользователя вопросами при вызове команды генерации проекта.
Включение в проект всех дотфайлов
Известная проблема maven архетипа - он не копирует в итоговый проект dot-файлы (.gitignore, .gitlab-ci.yaml и т.д.). Вместо того чтобы переименовывать их в что-то вроде dot.gitignore, можно добавить в pom.xml архетипа плагин, а сами файлы описать в archetype-metadata.xml
<!-- archetype-metadata.xml --> <fileSets> <!-- другие папки проекта --> <fileSet encoding="UTF-8"> <includes> <include>.gitignore</include> <include>.gitlab-ci.yml</include> <include>Dockerfile</include> <include>README.md</include> </includes> </fileSet> </fileSets>
<!-- pom.xml --> <build> <plugins> <!--Эти два плагина фиксят копирование dot-файлов в проект --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.4.0</version> <configuration> <addDefaultExcludes>false</addDefaultExcludes> </configuration> </plugin> <plugin> <artifactId>maven-archetype-plugin</artifactId> <version>3.4.0</version> <configuration> <useDefaultExcludes>false</useDefaultExcludes> </configuration> </plugin> </plugins> <extensions> <extension> <groupId>org.apache.maven.archetype</groupId> <artifactId>archetype-packaging</artifactId> <version>3.4.1</version> </extension> </extensions> </build>
Использование
Для генерации проекта нам осталось создать нам достаточно загрузить артефакт нашего архетипа в локальный репозиторий с помощью mvn install и вызвать генерацию проекта из созданного архетипа. Обратите внимание, что нам необязательно сразу передавать все параметры. Часть параметров могут быть запрошены Maven-ом в процессе выполнения команды. Работу с переменными мы описали ранее �� разделе requiredProperties файла archetype-metadata.xml.
mvn archetype:generate \ -DarchetypeGroupId=ru.example \ -DarchetypeArtifactId=demo-archetype \ -DarchetypeVersion=1.0.0 \ -DartifactId=demo-project \ -DmainClassPrefixName=DemoProject
