Ты не видел тут американскую подлодку? Видел. Куда она поплыла? Курс Зюйд-Зюйд-Вест. Ты не умничай, пальцем покажи.
(С) бородатый анекдот
За работу Уатт потребовал 1000 фунтов, и когда у него попросили счет, он написал: «Удар кувалдой — 1 фунт, знания, куда ударить, — 999 фунтов»
(С) исторический анекдот
За время карьеры, чаще всего сталкивался с ситуацией — когда получаешь проект в котором непонятно, где «вход», где «выход» и один только вопрос — «куда ткнуть?!». Товарищи, давшие проект, говорят — «это делал Вася, но он уже уволился, а на звонки не отвечает и никто не знает что к чему». Знакомая ситуация? Так же, после перебросок между многочисленными проектами бывало — когда смотришь непонимающе на код, а в коммитах автором видишь своё имя, но уже не помнишь вообще что к чему и как писал. Знакомо? В какой‑то момент меня стала доставать такая ситуация и я сделал себе небольшую автоматизацию, что бы снизить количество проблем и ненужных нагрузок на мозг, это — карта проекта.
Описание проблемы и стартового решения
В 2015м году, устав от постоянного отсутствия документации по проектам, я сделал себе специальный плагин и редактор позволяющие мне хранить описание проектов и неформализованные знания в виде интеллект‑карт (mind maps). Они оформляются текстовыми MMD файлами (сходными по формату с Markdown) и хранятся прямо среди исходных текстов проектов, попадая и в системы контроля версий. Об этом я уже писал статью на хабре и желающие могут ознакомиться по ссылке.
Мой плагин для работы с интеллект‑картами позволяет их формировать и редактировать вручную, но бывало «протухали» ссылки после рефакторинга и переименования исходных текстов. Была идея подключиться к подсистемам IDE и отслеживать редактирование исходников, внося изменения динамически в карты, но это и непросто и к тому же не универсально. Мне захотелось генерировать карту приложения автоматически, на базе проставленных меток и не быть привязанным ни к средам разработки, ни к билд‑системам.
В Java для этого идеально подходит аннотирование исходных текстов в нужных местах и как универсальное средство обработки — процессоры аннотаций, которые можно подключить прямо к Java компилятору. Так что напросился логичный вывод — сделать набор аннотаций для пометки нужных и важных мест в исходниках проекта и к нему сделать обработчик, который, в процессе компиляции, строит карту опираясь на помеченные места. Это позволило отвязаться от IDE в процессе и дать возможность в билд‑системах просто генерировать документацию по запросу, например через активацию профиля в том же Maven.
Я хотел бы кратко показать как это работает, на примере известного очень многим проекта Spring Pet Clinic. Добавим специальный профиль в Maven проект и будем получать (по запросу) карту размеченных «точек интереса» этого проекта.
Подготовка проекта
Начнем с клонирования проекта из GitHub
git clone git@github.com:spring-projects/spring-petclinic.git
Проект поддерживает одновременнно и Gradle и Maven билд-системы, но я буду показывать только на примере Maven, так как пользуюсь в основном им (для Gradle тоже есть вариант на wiki-странице проекта).
Сбилдим проект для уверенности, что проект не содержит ошибок. В Maven это производится вводом команды mvn package (активной должна быть директория содержащая pom.xml проекта).
mvn package
Добавляем зависимости
Первым делом добавляем зависимость с аннотациями в проектный файл pom.xml (это дескриптор проекта для Maven).
<dependency>
<groupId>com.igormaznitsa</groupId>
<artifactId>mind-map-annotations</artifactId>
<version>1.6.3</version>
<scope>provided</scope>
</dependency>
Добавленный пакет содержит весь требуемый набор аннотаций помеченных как "уровня исходных текстов", то есть компилятор не поместит их в итоговые скомпилированные файлы.
Настройка компилятора
Следующий шаг — добавить профиль, подключающий процессор аннотаций к процессу компиляции. Процессор будет извлекать информацию из аннотаций и формировать нужные нам документы при активации этого профиля. Профиль нужен, что бы получать документы по запросу, а не включать их генерацию в каждое построение проекта, хотя и такое тоже можно.
<profile>
<id>mmddoc</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<annotationProcessorPath>
<groupId>com.igormaznitsa</groupId>
<artifactId>mind-map-annotation-processor</artifactId>
<version>1.6.3</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</profile>
Мы получили в проекте дополнительный Maven-профиль mmddoc который можем активизировать, добавив в список активных профилей при построении проекта.
mvn clean package -Pmmddoc
При выполнении проект строится, но никаких изменений в процессе и результатах мы пока не видим.
Начинаем разметку
«Что бы получить что то нужное, надо добавить что то нужное» — нам надо добавить аннотации‑метки. Первая важная аннотация, которая запускает процесс, это аннотация com.igormaznitsa.mindmap.annotations.MmdFile. Именно она говорит процессору, что следует создать MMD файл. Добавим её в главный класс, запускающий приложение PetClinicApplication. Но нам мало просто добавить эту аннотацию, мы должны прописать имя файла и указать корневой элемент карты, описывающий приложение, так что выглядит это в коде вот так.
@MmdFile(fileName = "PetClinic", uid = "MAIN_APP",
rootTopic = @MmdTopic(title = "Pet clinic application"))
@SpringBootApplication
@ImportRuntimeHints(PetClinicRuntimeHints.class)
public class PetClinicApplication {
public static void main(String[] args) {
SpringApplication.run(PetClinicApplication.class, args);
}
}
Мы задали целевое имя файла PetClinic, задали ему уникальный идентификатор MAIN_APP который может использоваться в других аннотациях если потребуется и добавили корневой элемент содержащий текст Pet clinic application. Вроде всё работает и в исходных текстах при вызове генерируется MMD файл содержащий карту, которую может отрисовать плагин для NetBeans или Intellij IDEA.
Настраиваем пути к файлам по ссылкам
Но при щелчке по иконке файла в карте мы никуда не переходим и видим сообщение об ошибке «файл не найден». Это потому, что нам надо настроить пути для генерации. Плагин ждет, что ссылки будут относительными путями от корневой директории проекта. Добавляем к нашему процессору параметр, описывающий путь к корню проекта. Проект одномодульный, поэтому путь тривиальный и просто берется из project.basedir предоставляемой Maven.
<compilerArgs combine.children="append">
<compilerArg>-Ammd.file.link.base.folder=${project.basedir}</compilerArg>
</compilerArgs>
Опять перестраиваем проект с профилем mmddoc и получаем файл с корректно работающей ссылкой, которая открывается при кликом мыши и позиционирует нас на ту самую строку, где расположили нашу аннотацию. Теперь наша карта не даст нам никогда забыть — «где же у нас находится точка запуска приложения?», но этого маловато.
Добавляем больше информации на карту
Пора добавить на карту более ценную информацию — например информацию об HTTP контроллерах и их опубликованных методах. Начнем пожалуй с контроллера «ветеринара» в классе VetController. Идем в этот класс и добавляем к его заголовку «хитрые аннотации».
@MmdFileRef(uid = "MAIN_APP")
@MmdTopic(title = "Vet", path = { "Controllers" })
@Controller
class VetController {
Что же они значат? Первая аннотация MmdFileRef. Она говорит процессору, что для всех MMD узлов, найденных в классе, следует использовать файл документации с идентификатором MAIN_APP. Если еще помните, это тот самый идентификатор который мы присвоили файлу карты в PetClinicApplication. Следующая аннотация MmdTopic. Она создает на карте узел с текстом Vet и ссылкой на строку в файле где расположена. Но что бы выделить, что это «контроллер» и поместить его в раздел Controllers, мы задаем путь где указываем, что данный узел на карте должен располагаться как дочерний у узла с текстом Controllers (процессор сам автоматически создаст такой узел если не найдет). Результат нового построения MMD документа дает нам следующее изменение карты
Наша карта уже начала нести смысл и теперь любой может, открыв её, сразу понять где главный элемент приложения, а где находится контроллер для Vet. Всего парой кликов на карте перейдя в нужную точку кода. Но давайте добавим больше информации по контроллеру. Там имеются методы обрабатывающие HTTP запросы, их стоит нанести на нашу карту. Для этого, к найденным HTTP методам, добавим аннотации MmdTopic
@MmdTopic(title = "showVetList", note = "GET /vets.html")
@GetMapping("/vets.html")
public String showVetList(@RequestParam(defaultValue = "1") int page,
Model model) {
...
}
@MmdTopic(title = "showResourcesVetList", note = "GET /vets")
@GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() {
...
}
После повторного построения проекта, наш документ изменился.
Теперь карта несет полезную информацию и нанятый джун Петя, при требовании подправить «что‑то там с показом списка ветеринаров», не будет рыдать и клясть судьбу, пытаясь выяснить — «дяденьки! Какой класс отвечает за показ ветеринаров в нашем проекте?! В каком месте?!». Он просто откроет карту и ткнет мышью, попав в нужное место проекта. Обратите внимание, что у добавленных узлов еще и появились какие‑то записи рядом с файловыми ссылками. При щелчке на них, Петя увидит тип HTTP метода и его путь запроса, т. е. ту информацию которую мы указали в аннотации в поле note.
Добавим на карту еще один контроллер
Давайте добавим еще один контроллер, на этот раз — контроллер владельца животного. Он расположен в классе OwnerController. Сначала опять добавим ссылку на главный файл документа, что бы указать где хранить узлы определенные в классе. Затем определим базовый узел класса с указанием, что он должен так же лежать в пути с узлом Controllers
@MmdFileRef(uid = "MAIN_APP")
@MmdTopic(title = "Owner", path = { "Controllers" })
@Controller
class OwnerController {
...
}
На карте приложения добавился еще один узел, описывающий "Владельца"
Добавим метки ко всем его HTTP методам. Итоговая карта стала гораздо насыщенней
Рассмотрим метки метода
Давайте рассмотрим - что же мы такого сделали на примере метода showOwner
@MmdTopic(title = "showOwner", note = "GET /owners/{ownerId}")
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@MmdTopic(title = "ownerId")
@PathVariable("ownerId") int ownerId) {
...
}
Обратите внимание, что MmdTopic аннотация была добавлена и к аргументу метода описывающему переменную HTTP пути и мы на полученной карте видим эту отметку (а кликнув на её файловой ссылке, можем сразу перейти на точку определения параметра).
Настраиваем папку для итоговых документов
Всё вроде хорошо, но нас не очень устраивает, что полученный документ расположен в папке с исходными текстами, давайте вынесем его в какую то более подходящую папку. Для этого мы добавляем в параметры процессора аннотаций пару дополнительных параметров.
<compilerArg>-Ammd.target.folder=${project.basedir}/.projectKnowledge</compilerArg>
<compilerArg>-Ammd.folder.create=true</compilerArg>
Первый говорит процессору где хранить сгенерированные документы, а второй разрешает его автоматически создавать. Я выбрал .projectKnowledge, потому что плагины для IDE исторически автоматически распознают эту папку и её содержимое можно найти через специальную форму в той же Intellij IDEA.
Вот в принципе и всё нужное для минимального использования инструмента. Можно менять цвета, расположения, добавлять URL в узлы и «эмотиконы», но это уже «сахар» и с этим можно играться самостоятельно. Главное, что теперь у нас есть минимальная карта приложения, которая всегда может быть сгенерирована по запросу или обновлена при построении проекта и заинтересованное лицо сразу увидит наличие контроллеров и их HTTP методы. Любые изменения в позиционировании или именах файлов, любой рефакторинг, всё это после перестройки карты будет отображено и не надо будет лазать по текстам и клясть «устаревшую документацию».
Работа с многомодульными проектами
PetClinic это одномодульный проект с простой иерархией. Не так часто в энтерпрайз мире мы имеем дело с простыми проектами, как правило мы имеем дело с многомодульными иерархиями проектов. Плагин IDE, отображающий карту, рассчитывают путь файловых ссылок от корня самого верхнеуровневого проекта, что неочень совместимо с использованием maven параметром project.basedir, который содержит путь к директории текущего обрабатываемого проекта. Для таких таких случаев я использую специальный плагин, позволяющий мне получить путь к корневому родительскому модулю и сохранить его в выбранном свойстве. Выглядит полный maven профиль так:
<profile>
<id>mmddoc</id>
<properties>
<mmdDoc.folder>.projectKnowledge</mmdDoc.folder>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.commonjava.maven.plugins</groupId>
<artifactId>directory-maven-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<id>directories</id>
<goals>
<goal>highest-basedir</goal>
</goals>
<phase>initialize</phase>
<configuration>
<property>mmd.basedir</property>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<annotationProcessorPath>
<groupId>com.igormaznitsa</groupId>
<artifactId>mind-map-annotation-processor</artifactId>
<version>1.6.3</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<compilerArgs combine.children="append">
<compilerArg>-Ammd.file.link.base.folder=${mmd.basedir}</compilerArg>
<compilerArg>-Ammd.target.folder=${project.basedir}${file.separator}${mmdDoc.folder}</compilerArg>
<compilerArg>-Ammd.folder.create=true</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
Плагин directory-maven-plugin ищет самую верхнеуровневую директорию проекта и помещает её путь в свойство mmd.basedir, которое я потом использую как базовый путь для генерации ссылок.
Помимо всего прочего, иногда полезно включить очистку папки с документами в фазу инициализации построения проекта. Это можно сделать, добавив следующий вызов maven-clean-plugin
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>${project.basedir}${file.separator}${mmdDoc.folder}</directory>
<followSymlinks>false</followSymlinks>
</fileset>
</filesets>
</configuration>
<executions>
<execution>
<id>clear-mmd-folder</id>
<phase>initialize</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<excludeDefaultDirectories>true</excludeDefaultDirectories>
</configuration>
</execution>
</executions>
</plugin>