|
Professor Seleznov
|
Привет, Хабр! Меня зовут Михаил Поливаха. Я думаю, что в Хабе Spring АйО меня уже относительно знают. В рамках Spring АйО мы довольно часто занимаемся подбором технического материала и его ревью. Сам же я довольно регулярно выступаю на конференциях, контрибьючу в Open Source и т.д. Также, часто наши материалы крутятся вокруг Java разработки и конечно же Spring-а. И данной пост, хоть и будет с одной стороны сильно связан с Java и Spring Framework, но, тем не менее, не похож на остальные. На днях произошло довольно знаковое событие. Мы с небольшой командой примерно год писали инструмент, который призван существенно упростить весь процесс отладки, тестирования и мониторинга Spring Boot приложений в production. И вот этот проект наконец-то получил первый Milestone Релиз. Проект называется Axelix. Думаю, что важно сразу отметить: ядро проекта Axelix является Open Source продуктом и доступно на GitHub. Проект будет следовать модели Open Core. Поэтому Вы можете спокойно ставить его себе в контур так же, как ставите, например, Grafana. В данном посте я хочу рассказать про историю проекта, то, зачем он нужен, и как мы собтсвенно вообще к нему пришли. В нём будет анализ причин появления Axelix и глубокий обзор конкретного кейса решения повседневных проблем в рамках разработки на Spring Boot. Надеюсь, что Вам будет интересно. Ну что же, начнём  Сотояние Индустрии Enterprise Софта Типичный enterprise имеет много внутреннего софта. Этот софт решает разные задачи и обычно написан на нескольких языках программирования — Java, Go, C# и т.д. Один из самых популярных ЯП для server-side/backend-приложений в крупных компаниях — это Java. На это есть много исторических причин, но факт остаётся фактом. Кроме того, самый популярный фреймворк для написания backend-приложений на Java — бесспорно Spring Boot. Очень упрощённо: это дополнительный слой поверх Spring Framework, который упрощает его конфигурацию (хотя, сейчас Spring Boot уже гораздо больше, чем просто автоконфигурация). По ряду причин это безоговорочно самый популярный выбор в enterprise. Например, Netflix мигрировал свою экосистему (более 3000 приложений) на Spring Boot 3 и планирует оставаться на Spring Boot. Итого: Spring Boot победил. Это очень популярный выбор на backend при разработке на Java. Это то, что у нас есть сегодня и с этим странно спорить. Мотивация и Pain Point Что интересно: Spring Framework изначально начинался как IoC-контейнер с dependency injection в качестве главной фичи, а сейчас это совсем другое — намного, намного больше. Сегодня Spring Framework / Spring Boot — целая экосистема, которая поддерживает весь технологический стек нашего приложения:
- Обработка HTTP-запросов
- Работа с RDBMS — от управления транзакциями до ORM
- Отправка сообщений в message broker-ы и многое другое
В результате экосистема Spring Framework существенно выросла в сложности. В современных Spring Boot microservice-ах у нас очень много движущихся частей. Например:
- В приложениях много bean-ов. Часть приходит из Spring Boot и auto-configuration, часть — из внутренних платформенных библиотек, которые мы используем в компании, а часть мы создаём сами.
- У нас довольно много properties, которые могут приходить из разных источников: из файла application.yaml, из Spring Cloud Config, из environment переменных внутри Pod-ов в K8S, из аргументов командной строки и так далее.
- Конечно же, у нас много proxy! И все они работают друг с другом: transactional proxy, proxy для OpenFeign client-ов, proxy для Kafka Listener-ов и т.д.
Идея в том, что Spring Framework стал довольно большим и породил много взаимосвязанных частей, где каждая влияет на другую (например, properties влияют на создание bean-ов, созданные bean-ы могут подтянуть ещё properties и т.д.). Есть даже такое мнение, что из-за всего этого Spring Framework превратился в большого неукротимого монстра. Подумайте сами: почти на любой Java-конференции — и внутри России, и даже за рубежом (говорю как спикер на международных конференциях, в том числе) — добрая половина докладов посвящена Spring Framework, тому как он устроен, или похожим темам. Почему? Потому что Spring а) везде, и б) он сложен! И, следуя из этого, за годы написания Spring Boot-приложений я с почти 100%-ной уверенностью могу сказать: когда мы, Java-разработчики на Spring Boot, деплоим приложения в любое окружение, мы снова и снова сталкиваемся с одними и теми же проблемами. Например:
- Мы делаем HTTP-запросы внутри транзакций и исчерпываем пул db-connection-ов.
- Мы создаем N + 1 и сталкиваемся с проблемами lazy loading в Spring Data JPA.
- Мы дебажим ситуации, когда в @Value / @ConfigurationProperties подтянулось не то property.
- Мы дебажим случаи, когда наш @Bean был “подавлен” из-за какого-то bean-а из @AutoConfiguration или платформенной либы и так далее…
Все эти проблемы реальны. Если Вы писали Spring Boot-приложения хотя бы пару лет, уверен, хотя бы часть из них Вам знакома. А теперь: что, если я скажу, что боль от разработки Spring Boot backend-ов можно облегчить? Вдумайтесь: сегодня по всему миру сотни и сотни команд пишут Spring Boot backend-ы и снова и снова борются с одними и теми же проблемами. Можно ли это как-то решить? Давайте попробуем разобраться на примере. Типичный День Spring Boot-разработчика Чтобы глубже понять, что именно Axelix пытается решить, пройдёмся по паре примеров Java-кода. Представьте, что в приложении есть следующий код ниже. У нас есть Spring бин, который мы используем, чтобы получить набор permissions, которыми обладает гипотетический клиент:
@Slf4j @Service public class PermissionsResolver { @Value("${permission.caching.enabled:true}") private boolean cacheLookupsEnabled; @Autowired private ExternalPermissionsResolver delegate; @Autowired private PermissionsCache cache; @Autowired private DetailsResolver detailsResolver; @PostConstruct void init() { log.debug("Permissions cache lookup enabled: {}", cacheLookupsEnabled); } @Transactional public Set<PermissionDto> resolveClientPermissions(Client client) { try { Set<PermissionDto> permissions = delegate.resolve(client); for (Permission permission : permissions) { PermissionDetails details = detailsResolver.resolvePermissionDetails(permission); permission.setDetails(details); } return permissions; } catch (SomeExpectedException e) { if (cacheLookupsEnabled && client.getStatus() == ClientStatus.EXISTING) { log.debug("For the client '{}' taking permissions from the cache", client.getId()); return cache.get(client.getId()); } else { throw e; } } } }
Это классический Java-код на стеке Spring Boot. Давайте честно: мы же все такое видели. Все такой код отлаживали, все много-много раз дорабатывали такой код. И во время регрессионного тестирования QA-инженер приходит и говорит:
Привет! На dev/qa/sandbox/pre-prod окружении я не могу создать XYZ — пишет, что у моего client-а нет нужных permissions
Вы открываете IDE (или AI Agent, что уж там 😈) и пытаетесь разобраться в кодовой базе. Ищете, что может пойти не так. Часть 1. Смена Уровня Логирования А вариантов того, что может пойти не так в Spring Boot приложении — просто вагон. Например, может оказаться, что мы берём client-ов из кэша. Ну, по крайней мере, кэш должен быть включён, верно? Или нет… И, как всегда, нужный нам log — в DEBUG, а не в INFO. И, конечно, logback — дефолтная система логирования под Spring Boot — его отфильтровывает и не пропускает. Разве не было бы круто временно поднять уровень логирования для этого класса до DEBUG? Без редеплоя, буквально на 10 минут? Axelix позволяет временно изменить уровень логирования — только для отладки, — а потом откатить его назад. Кстати, очень скоро Axelix сможет откатить уровень логгирования сам, без необходимости разработчику вручную возвращать всё как было. Часть 2. Загадочное Значение Property Вы копаете дальше и понимаете: поведение некорректно, потому что на pre-prod мы вообще не должны брать client-ов из кэша. Мы должны упасть и вернуть соответствующий status code. Теперь вопрос: почему, чёрт возьми, кэш вообще включён? Мы же явно отключили его через переменную окружения… Может, где-то есть другой property (например, из Spring Cloud Config), который переопределяет наш? А что, если Spring вообще не увидел нашу переменную окружения, отключающую кэш? Это открытые вопросы, и поверьте, Вы не один такой. С этим сталкивается очень много людей. Axelix может не только показать, какие properties знает Spring, но и сказать, какое property в итоге победило и подавило остальные — и откуда оно пришло. Часть 3. Зоопарк Bean-ов Ладно, Вы выяснили, что property на самом деле пришло из VCS через Spring Cloud Config, и какой-то инженер просто его поменял и забыл сказать (упс, как неловко-то вышло). Вы, скаля зубы, выключили кэширование и попросили QA перезапустить сценарий. Client, очевидно, по-прежнему без permissions, но хотя бы теперь мы отдаём ошибку, а не ответ из fallback-кэша. Теперь нужно понять, почему permissions нет. QA мамой клянётся, что добавил их в базу данных или попросил кого-то это сделать. Вы ему доверяете, но (не на все 100%, конечно) и снова открываете IDE. И вот что находите:
public interface ExternalPermissionsResolver { Set<PermissionDto> resolve(Client client); } @Component @ConditionalOnProperty(prefix = "permissions.resolver", value = "source", havingValue = "legacy", matchIfMissing = true) @ConditionalOnMissingBean(ExternalPermissionsResolver.class) public class LegacySystemPermissionsResolver implements ExternalPermissionsResolver { @Override public Set<PermissionDto> get(Client client) { // implementation } } @Component @ConditionalOnProperty(prefix = "permissions.resolver", value = "source", havingValue = "database") @ConditionalOnDatabaseAvailable @ConditionalOnMissingBean(ExternalPermissionsResolver.class) public class DatabaseSystemPermissionsResolver implements ExternalPermissionsResolver { @Override public Set<PermissionDto> get(Client client) { // implementation } }
Итак, на pre-prod мы не хотим использовать старый LegacySystemPermissionsResolver — мы от него избавляемся. Есть ли шанс, что он всё ещё используется? По крайней мере, мы можем посмотреть properties и их фактические значения — Axelix уже помогает с этим. Однако это ещё не отвечает на главный вопрос: а мы на 100% уверены, что используем DatabaseSystemPermissionsResolver? А что, если нет? Здесь Axelix может не только сказать, какие bean-ы реально активны, но и объяснить, откуда они взялись. Более того, Axelix может объяснить, почему bean-ы, которые мы ожидали увидеть созданными, на самом деле не были созданы. Например, наш DatabaseSystemPermissionsResolver мог не создаться, потому что кастомная conditional-аннотация @ConditionalOnDatabaseAvailable не прошла, — и в итоге создался fallback LegacySystemPermissionsResolver. Часть 4. Hibernate всегда рядом, чтобы всё испортить Уже измотанный, измучанный, бедняга, Вы решили и эту проблему. Причина сломанных permissions — использование не того ExternalPermissionsResolver. Тестирование на pre-prod завершено, наконец. Столько времени ушло на отладку — это просто безумие!. Но разработчики для этого и существуют, чтобы страдать. Вы это принимаете и возвращаетесь к работе. Только собрались с мыслями, сели за тикет — прилетает отчёт от нагрузочного тестирования:
95-й перцентиль latency улетел в космос: 5–6 секунд! Такое нельзя в прод катить!
Приложение под нагрузкой почему-то начинает тормозить. Вы не знаете, в чём дело. Но раз мы меняли только логику разрешения permissions, проблема, должно быть, где-то там. Вы снова внимательно смотрите на код и понимаете:
Чёрт! Снова N + 1! DatabaseSystemPermissionsResolver возвращает entity без JOIN FETCH-нутой коллекции permissions!
Дъявол! А ведь проблема с N + 1 коварна тем, что её не так просто поймать в тестах! Конечно, Hibernate даёт статистику для её детекта, но никто же не заморочился писать тесты с глубоким анализом Hibernate statistics! И этот код успешно прошёл:
- Code Review
- Quality Gate
- Функциональное тестирование от QA
- Security проверки от Sonar Qube и т.д.
Все барьеры пройдены! А теперь… Теперь нужно менять код, мержить, перетестировать, заново гонять security scan. Это такая боль! В агонии начинаете чувствовать, что ненавидите работу, а Hibernate — ещё больше, потому что всё так неочевидно! Но не переживайте — Axelix может помочь. Axelix может обнаружить N + 1 запросы на testing/qa/sandbox окружениях задолго до деплоя на pre-prod или стенд нагрузочного тестирования. Вот и всё. Более того, Вы не только можете обнаружить N + 1 запросы, но и точно сказать, какие запросы Hibernate выполнил под капотом в рамках этой @Transactional и сколько она длилась. Наконец, если не хотите использовать UI Axelix, в ближайшем будущем можно будет настроить экспорт этих данных в любое APM-решение на Ваш вкус — от простого экспорта дополнительных Prometheus Metrics, до современного Open Telemetry — и смотреть те же данные из Axelix в привычной Grafana, например. Интеграция с AI. MCP Server. Фичи выше — это примерно ~30% того, что Axelix умеет сейчас; там гораздо больше. Я не перечисляю всё, иначе статья станет очень длинной. Но есть ещё один слон в посудной лавке, которого нельзя игнорировать — AI. Сегодня мы все пользуемся AI Agent-ами: Cursor, Codex, Claude и т.д. Давайте честно: они занимают значительную часть нашей жизни как Software Engineer-ов. В Axelix мы это прекрасно понимаем; у нас AI:
- Помогает ревьюить код в monorepo
- Помогает писать интернационализированную документацию и т.д.
Мы признаём влияние и ценность AI. Поэтому во всех дистрибутивах Axelix идёт встроенный MCP server, который отдаёт все описанные выше (и не только) возможности AI Agent-у через embedded MCP server. Хотите отлаживать проблемы через AI Agent — пожалуйста. Правило, которому мы следуем: всё, что доступно человеку через UI, также доступно AI Agent-ам через MCP server. Сравнение со Spring Boot Admin Уверен, опытные разработчики слышали про Spring Boot Admin. Этот инструмент работает как proxy к существующим Spring Boot Actuator endpoint-ам и даёт часть возможностей, которые умеет Axelix. Мы настолько часто объясняли, чем Axelix отличается от Spring Boot Admin, что даже сделали отдельную страницу в документации, где объясняем, какие проблемы со Spring Boot Admin мы видим и какую роль Axelix играет в общей картине. Прочитайте, если интересно. Заключение. Что дальше Повторю: мы только что выпустили первый milestone. GA-релиза пока нет, но мы планируем его во второй половине лета 2026 года. Чтобы быть в курсе, мы завели telegram-канал для новостей о фичах и релизах. Подписывайтесь, если хотите следить за проектом. У нас уже есть pilot-команды, которые поставили Axelix в свои окружения — как Helm Chart, Docker image или даже просто JAR. Мы по-прежнему активно обрабатываем feedback от пилотных команд. Если хотите поставить Axelix — Вы уже можете это сделать, документация доступна на русском и английском. В этом случае Вы сможете дать feedback и существенно повлиять на развитие Axelix. Мы активно полируем документацию; если понадобится помощь — пишите напрямую администраторам Telegram-канала проекта Axelix. Поможем с установкой. Надеюсь, статья была интересной. Если любопытно, как это устроено — исходники Axelix Core на GitHub. Изучайте на здоровье! P.S: Мы приветствуем контрибьюторов! Хотите помочь — загляните в contribution guidelines нашего проекта. Будем рады увидеть PR/Issue от Вас когда-нибудь!-Источник
|