Мотивация
В этом документе описаны основные проблемы, которые Axelix решает для разработчиков на Spring Boot и для организаций. На реальных примерах мы разберём болевые точки, вдохновившие создание Axelix.
Мотивация. Пролог
В типичном крупном бизнесе много программного обеспечения, написанного внутри компании. Это ПО решает разные задачи и обычно пишется на нескольких языках программирования — например, JavaScript, Java, Go и т.д. Один из самых популярных вариантов для серверных/бэкенд-приложений в крупных предприятиях — это Java. Особенно это заметно в банковской отрасли.
Дальше — самый популярный фреймворк для написания Java-приложений на бэкенде, бесспорно, Spring Boot. Это дополнительный слой поверх Spring Framework, упрощающий его конфигурацию. Это безоговорочно самый популярный выбор в энтерпрайзе по разным причинам. Например, Netflix мигрировал свою экосистему (более 3000 приложений) на Spring Boot 3. То есть идея в том, что Spring Boot — очень популярный выбор на бэкенде при разработке Java-приложений.
Мотивация. Болевая точка
Как уже было сказано, Spring Boot начинался просто как дополнительный слой абстракции над Spring Framework, но сейчас это значительно больше. И все Java-приложения, работающие на Spring Boot, оперируют одними и теми же абстракциями Spring Boot.
Например:
- Каждое Java-приложение на Spring Boot состоит из набора Bean'ов — это Java-объекты, управление которыми
- в той или иной мере осуществляет Spring Framework.
- У каждого Spring Boot-приложения есть
ApplicationContext, основное назначение которого — управлять бинами приложения - и общим состоянием работающего Spring Boot-приложения.
- У каждого Spring Boot-приложения есть довольно много свойств (properties), которые могут приходить из разных источников
- (файл
application.properties, переменные окружения процесса ОС, системные свойства Java и т.д.).
Идея в том, что Spring достаточно большой, и в нём много подвижных частей, каждая из которых влияет на другие (например, свойства влияют на создание бинов, а созданные бины могут подтянуть новые свойства и т.д.).
И одна из главных причин существования этого проекта в том, что если Spring Boot-приложение работает локально как ожидается и все тест-кейсы проходят, это вовсе не означает, что то же приложение будет идеально работать после развёртывания на проде или тестовом окружении. Мы все знаем, что предположение «всё будет хорошо» попросту неверно.
Из этого следует, что мы как Java-разработчики, использующие Spring Boot, при деплое наших приложений на любое окружение постоянно сталкиваемся с одними и теми же проблемами снова и снова. Давайте разберём пару примеров, демонстрирующих эту проблему.
Пример 1. Точечное логирование
Чтобы глубже понять, что именно решает Axelix, разберём пару примеров Java-кода. Представьте, что в нашем приложении есть следующий Java-класс. Это Spring Bean, который мы используем для получения набора прав гипотетического клиента:
@Slf4j
@Service
public class PermissionsResolver {
@Value("${permission.caching.enabled:true}")
private boolean cacheLookupsEnabled;
@Autowired
private ExternalPermissionsResolver delegate;
@Autowired
private PermissionsCache cache;
@PostConstruct
void init() {
log.debug("Permissions cache lookup enabled: {}", cacheLookupsEnabled);
}
public Set<Permission> resolveClientPermissions(Client client) {
log.debug("Resolving permissions for the client {}", client.getId());
try {
return delegate.resolve(client);
} catch (SomeExpectedException e) {
// if we can, we do a cache lookup
if (featureFlag && client.getStatus() == ClientStatus.EXISTING) {
return cache.get(client.getId());
} else {
throw e;
}
}
}
}Мы написали этот код. Мы написали к нему тесты. Мы протестировали его локально. Всё работало. Теперь мы деплоим этот код на наш sandbox-окружение.
Но по закону Мёрфи:\ Всё, что может пойти не так, обязательно пойдёт не так\ Что-то некорректно работает на нашем
sandbox/тестовом окружении. Локально мы тестировали — всё работало, а на тестовом окружении — внезапно нет. И хотя в
классе есть лог-стейтменты, которые помогли бы нам понять, что на самом деле происходит внутри развёрнутого приложения,
они работают на уровне DEBUG, а у нас включён только уровень INFO. Было бы здорово временно(!) изменить уровень
логирования «на лету» только для этого конкретного Java-класса или конкретного Java-пакета. Можем ли мы изменить
уровень логирования в конфигурации приложения и переразвернуть его? Да, конечно, можем, но действительно ли мы хотим
проходить весь процесс пересборки и передеплоя только ради того, чтобы проверить логи? А если логи не помогут? Снова
собирать и передеплоивать?
В Axelix мы очень глубоко понимаем эту боль. И мы хотим предоставить способ её решить.
Пример 2. Источники конфигурации
Окей, предположим, что первую проблему мы решили и сумели включить логирование через Axelix UI для этого конкретного
Java-класса. Теперь у нас другая проблема. Допустим, мы поняли, что в нашем случае ошибка возникает потому, что в
свойство cacheLookupsEnabled было инжектировано некорректное значение. Мы ожидали, что для клиента XYZ данные будут
браться из кеша, но этого не произошло. И не произошло потому, что cacheLookupsEnabled оказался равен false.
Что нам теперь делать? Единственное, что мы знаем — это свойство выставлено в false, и сделали это не мы. Значит,
где-то, в каком-то источнике конфигурации нашего приложения оно было установлено в false. Но опять же — где именно?
И давайте честно: мы как разработчики в крупной компании далеко не всегда даже понимаем, как именно наше приложение разворачивается на окружении. Мы не всегда полностью понимаем, как наше приложение упаковывается в Docker-контейнер, как затем разворачивается через Helm-чарт в наших K8S-кластерах, какие дополнительные источники конфигурации применяются к нашему приложению и т.д. У нас есть какое-то понимание того, как разворачивается наше приложение, но это понимание неполное — и в этом основная проблема.
В Axelix мы не только даём возможность увидеть, из какого именно источника конфигурации пришло значение свойства, но и позволяем менять значение свойства «на лету» на живом Java-приложении, перезагружая контекст приложения на горячей JVM — это не требует пересборки/передеплоя.
Пример 3. Состояние приложения
Теперь мы изменили значение cacheLookupsEnabled на true, и всё должно работать, верно? Нет. Оно по-прежнему не
работает. И не работает потому, что PermissionsCache — это на самом деле интерфейс:
public interface PermissionsCache {
Set<Permission> get(UUID clientId);
}
@Component
@ConditionalOnProperty(prefix = "permissions.cache", value = "source", havingValue = "in-memory", matchIfMissing = true)
@ConditionalOnMissingBean(PermissionsCache.class)
public class InMemoryPermissionsCache {
@Override
public Set<Permission> get(UUID clientId) {
// implementation
}
}
@Component
@ConditionalOnProperty(prefix = "permissions.cache", value = "source", havingValue = "redis")
@ConditionalOnRedisPresent
@ConditionalOnMissingBean(PermissionsCache.class)
public class RedisPermissionsCache {
@Override
public Set<Permission> get(UUID clientId) {
// implementation
}
}
Думаю, все согласятся, что очень часто встречается ситуация, когда есть interface, описывающий контракт, и есть
несколько реализаций этого Java-интерфейса, каждая из которых — Spring Bean. Мы ожидаем, что эти Spring-бины будут
подниматься только при выполнении определённых условий. Всем известные ConditionalOnProperty,
ConditionalOnMissingBean и т.д. знакомы практически каждому Spring-разработчику.
И предположим, что в нашем случае мы ожидали, что права данного клиента будут браться из Redis-кеша, а не из in-memory
структуры. Но на самом деле был опрошен InMemoryPermissionsCache. А мы вообще уверены, что это был
InMemoryPermissionsCache? Может быть, какая-то корпоративная библиотека, которую мы подключаем как зависимость,
приносит свой собственный ThreadSafeInMemoryPermissionsCache? Иначе говоря, мы хотим ответить на вопросы:
- Какие бины сейчас активны в Spring-приложении?
- Почему активны именно эти бины, а не другие?
Axelix помогает ответить и на это тоже.
Введение
Добро пожаловать в Axelix! Мы рады, что вы здесь.
Архитектура
В Axelix есть одно центральное приложение — Axelix Master, которое общается с каждым Spring Boot-сервисом, которым вы хотите управлять, через зависимость Axelix Starter на стороне сервиса. Веб-интерфейс и MCP-сервер живут внутри Master, поэтому одни и те же данные отдаются и браузеру, и AI-агенту из одного процесса.